"To do" app
This is an Active CSS implementation of the famous "To do" app.
The HTML and CSS for this has been taken from https://todomvc.com/. Check that website out to see how the same app is done in native JS and JS frameworks.
Note that this example makes use of the special Active CSS selector "<" which goes up the element parents in the DOM to the next matching selector:
.toggle:checked:click {
& < li {
add-class: .completed;
}
}
The up selector ("<") has not been implemented in regular CSS in browsers in any form for performance reasons related to the internal rendering engine, but in Active CSS it can be available.
This particular example is editable from inside the code editor. In the code editor it draws the result app in an iframe. The iframe itself does not have a URL, so using the regular method of "#/active" or "#/completed" on the URL is not practical for displaying the sections. Instead it stores the state of "All", "Active" and "Completed" in localstorage, along with the todo items themselves, so that when the page is reset or redraws it remembers what to display.
An annotated version of the config used, explaining the different aspects of functionality of the Active CSS used, follows below the code editor.
"To do" app
The code used in the example with comments:
/***
Declare the local storage toDoItems object if it isn't there already and set the option state to "all".
***/
body:not-if-defined(toDoItems):preInit {
var: toDoItems {} local-storage, optionsState "all" local-storage;
}
/***
Declare the item counter variable.
***/
body:preInit {
var: totalNotCompleted 0;
}
/***
Draw the items on reload if they are there and set the option class in the body tag. Set the id in the tag as it came from local storage and it has an id.
***/
body:init {
add-class: {optionsState};
@each toDoId in {toDoItems} {
.todo-list {
render-before-end: "<li id=\"{toDoId}\">{|toDoItem}</li>";
}
}
}
/***
Stop the browser from responding to the link clicks.
***/
a:click {
prevent-default: true;
}
/***
Set up the options so when they are clicked it puts a class in the body tag and sets a state too.
***/
#all:click, #active:click, #complete:click {
body {
set-class: .{@id};
var: optionsState "{@id}";
}
}
/***
When typing in a new entry, when the enter key is pressed, set a variable with the latest content and draw the item component, then empty the field.
***/
.new-todo:not-if-empty(self):keydownEnter {
var: latestEntry "{@value}";
.todo-list {
render-before-end: "<li>{|toDoItem}</li>";
}
set-property: value "";
}
/***
If the "toggle all" icon is clicked and it is check already, turn off the underlying checkbox on each item's toggle element and trigger a click event on each one.
***/
.toggle-all:checked:click {
.toggle {
set-property: checked "";
trigger-real: click;
}
}
/***
If the "toggle all" icon is clicked and it is not checked already, turn on the underlying checkbox on each item's toggle element and trigger a click event on each one.
***/
.toggle-all:not(:checked):click {
.toggle {
set-property: checked checked;
trigger-real: click;
}
}
/***
If the "clear completed" optionis clicked, remove all elements that have the class completed, and delete the item from the main toDoItems object.
***/
.clear-completed:click {
.completed {
var-delete: toDoItems["{@id}"];
remove: self;
}
}
/***
These events trigger an update which recalculates the number of items in the list.
***/
.todo-count:draw,
.new-todo:not-if-empty(self):keydownEnter,
.toggle:click,
.toggle-all:click,
.clear-completed:click {
~counter {
trigger: update after stack;
}
}
/***
This custom event calculates the number of items that are not yet completed based on whether the toggle button is checked for each item, and then triggers a display event.
***/
~counter:update {
var: totalNotCompleted 0;
.toggle:not(:checked) {
var: totalNotCompleted++;
}
trigger: display;
}
/***
If there is one item left, display "1 item left".
***/
~counter:if-var(totalNotCompleted 1):display {
.todo-count {
render: "1 item left";
}
}
/***
If there isn't one item left, display "n items left".
***/
~counter:not-if-var(totalNotCompleted 1):display {
.todo-count {
render: "{totalNotCompleted} items left";
}
}
/********************************/
/******** Item component ********/
/********************************/
@component toDoItem private {
/***
Draw the html for the component containing a reactive variable pointing to the todo item title.
***/
html {
<div class="view">
<input class="toggle" type="checkbox">
<label>{{toDoItems[thisID].title}}</label>
<button class="destroy"></button>
</div>
}
/***
Before the component is opened, if the id is in the tag because it came from local storage, set a variable thisID to be the id of the item.
***/
&:not-if-empty("{@id}"):beforeComponentOpen {
var: thisID "{@id}";
}
/***
Before the component is opened, if the id is not in the tag already, this must be a new item, so calculate an item id and set up a basic item in the toDoItems object.
***/
&:if-empty("{@id}"):beforeComponentOpen {
set-attribute: id "todo_{$RANDSTR32}";
var: thisID "{@id}", toDoItems[thisID] { title: "{latestEntry}", completed: false };
}
/***
After the component is drawn, if the item is completed, set the toggle switch of the item to checked so the CSS draws a line through it.
***/
&:if-var-true(toDoItems[thisID].completed):componentOpen {
add-class: .completed;
.toggle {
set-property: checked checked;
}
}
/***
Now the completed state is set, recalculate and draw the new total number of items by using a trigger to the event set up earlier.
***/
&:componentOpen {
~counter {
trigger: update;
}
}
/***
In this component, if the item is checked then set the containing li tag with a class of "completed", and set the state in the toDoItems object.
***/
.toggle:checked:click {
var: toDoItems[thisID].completed true;
& < li {
add-class: .completed;
}
}
/***
If the item is unchecked then remove the class of "completed" from the containing li tag, and set the state in the toDoItems object, and uncheck the toggle-all button.
***/
.toggle:not(:checked):click {
var: toDoItems[thisID].completed false;
& < li {
remove-class: .completed;
}
.toggle-all {
set-property: checked "";
}
}
/***
If the label is double-clicked, put an .editing class on the li container so the CSS hides the item view and draw a field for editing the item.
***/
label:dblclick {
& < li {
add-class: .editing;
}
.view {
render-after-end: "<input class=\"edit\" value="{toDoItems[thisID].title}">";
focus-on: .edit end-of-field;
display: none;
}
}
/***
When the person types into the field, the latest value property is put into the variable in the toDoItems object.
***/
.edit:input {
var: toDoItems[thisID].title {@@value};
}
/***
When the editing field first gets focus, put a clickoutside event on the field.
***/
.edit:focus {
var: originalString "{@@value}";
clickoutside-event: true continue;
}
/***
When editing, if the enter key is clicked or the person clicks outside the field, or if the destroy button is clicked, recalculate the total items again after doing everything else.
***/
.edit:keydownEnter, .edit:clickoutside, .destroy:click {
clickoutside-event: false;
~counter { trigger: update after stack; };
}
/***
If the "escape" key is pressed, restore the original value before editing started back into the item in the toDoItems object.
***/
.edit:keydownEscape {
var: toDoItems[thisID].title {originalString};
}
/***
When editing, if the enter key is pressed and the field isn't empty, the person clicks outside, or the escape key is pressed, remove the editing field and re-display the item view.
***/
.edit:not-if-empty({@@value}):keydownEnter, .edit:not-if-empty({@@value}):clickoutside, .edit:keydownEscape {
display: none;
& < li {
remove-class: .editing;
}
.view {
display: block;
}
remove: self;
}
/***
When editing, if the enter key is pressed and the field is empty or the person clicks outside, or the option button is clicked, remove the item from the toDoItems variable and remove the item from the DOM.
***/
.edit:if-empty({@@value}):keydownEnter, .edit:if-empty({@@value}):clickoutside, .destroy:click {
& < li {
var-delete: toDoItems["{@id}"];
remove: self;
}
}
}