Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 35 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/---------------------------- GENERAL INFORMATION ----------------------/
/--------------------------------- THE TREE -----------------------------------/

THE TREE... (structure of the html elements)

Expand All @@ -17,27 +17,42 @@ THE TREE... (structure of the html elements)
-
-> (whatever tab gets clicked. Ex. <Calendar />, <Journal />, etc.)

My understanding of the basic datastructures for the app (this can be changed however makes sense)....

Model:
The model contains the single source of data for the app. Anything that wants to access some part of the
data calls "model.subscribeTo(<reference to itself>, "tasktitles")" and passes it a reference to itself (generally
componentDidMount() or or the constructor is a good place to put this) with a string indicating which data source
it wants to know about (TaskList wants to know about the "task_titles"). The subscribing component needs to implement
an "onChange(newData)" method that describes what it should do with the new data (generally involving a call to
this.setState() ).

When a tab wants to change some part of the data, it calls "model.request(action)", and passes in an 'action' object,
which looks (something) like this, // action = {code: "task_analytics_update", data: { <whatever the model needs to do that> }} //

note - I think it would be easiest to track component specific changes within your component, and then send the resulting
composite object (probably something like // action.data = {analytics: {all the data}}) // ) as a whole,rather than send actions to the model for each minor change as it occurs.

To render your component for debuggging/testing (see ExTab for an example component) you can go into TabDisplay (the component
that renders whichever tab gets clicked) and add <YourComponent /> inside the ".displayContainer" div. If you want to pass it test
arguments, you can call it with <YourComponent testData={reference to some test data}> and then in your constructor, you can access
this data with 'props.testData'.
RENDERING FOR DEBUGGING:
To render your component for debuggging/testing (see ExTab for an example component) you can go into TabDisplay (the component
and replace <ExTab /> with your component (and pass it any dummy data you will need).

/---------------------------------- MODEL ------------------------------------/

Model:
The Model manages the interactions between components (mostly tabs) and all the data for the app (list of all tasks ('taskList'
list of all task titles, paired with their 'key' ('titlesKeyPairs')), and the most recently clicked task ('currentTask')). It handles updates to the data (add, edit, remove) in response to actions (clicks, form entries, etc.) that occur from anywhere in the app.

What I think 'tasks' are:
task = {
title: "<what is it called?>",
key: <unique numeric identifier, related to the index when it is first read from the DB>,
attrs: { < general information, ie. notification level, settings, etc. > },
tabs: { < all information that the tabs need or have saved goes here > }
}

Components and the model:
Basically, I don't think they should interact directly with the model, I think it is easier to test and manage if
the components (tabs) get passed the single function that they need from the Model ('recordFinalState()'), rather than a reference to the entire thing.

-> The TabDisplay component (who's role is rendering tabs based on tab clicks) that renders your component will pass
in all the data from the currently selected task that has to do with your component, along with that task's 'key' property (a unique numeric identifier).

-> I think you should handle actions locally, meaning if a user wants to edit a journal entry, then record those changes in
your local copy of the Journal info for the currently selected task in your component's 'state'. Then in your component's 'componentWillUnmount()' method, pass the final version of that state, with all of its accumulated changes (or no changes if nothing changed) back to the Model, and it will update the appropriate task (via the Model's recordFinalState("<component's name>", this.state, key), which will be passed as a function into your component (again, through the 'props' argument in the
constructor)). (the functional version of the this.setState() method will come in handy, as if you use it, React will automatically re-render your component with the appropriately updated state).

*** I'll update ExTab to try and illustrate what I mean ***

*** if you think this is bonkers, slack me, I am happy to make changes to try and make it easier :) ***

*** For now, just pretend that you have access to all of the data from the currentTask that has to do with your individual
component, passed in through the 'props' argument in the constructor. ***

/------------------------------- OTHER STUFF? -------------------------------/

6 changes: 4 additions & 2 deletions src/App/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import React, { Component } from 'react';
import Frame from '../components/Frame';
import Model from '../Model/Model';
import './App.css';
import { getTestTaskListSmall } from '../TestResources/testutils'

class App extends Component {
constructor(props) {
super(props);
this.model = new Model();
// this reference to the model gets passed down through the tree
// this is just for testing..
this.model = new Model(true, getTestTaskListSmall());
}

render() {
Expand All @@ -15,7 +18,6 @@ class App extends Component {
<Frame model={this.model}/>
</div>
);

}

}
Expand Down
125 changes: 72 additions & 53 deletions src/Model/Model.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
/*
* This tracks state changes.
*/

import ObservableData from './ObservableData';
// just for testing :)
import { getTestTaskListSmall } from '../TestResources/testutils'
import Resources from './Resources';

/*
* This the single source of data for the app.
* This manages connections between components, data, and actions (updates, edits, deletion, etc.).
*
* Any components that need information can subscribe to the datasource that they need (and they
* will be updated (through their 'onChange()' method) when that data is altered).
Expand All @@ -16,62 +10,87 @@ import { getTestTaskListSmall } from '../TestResources/testutils'
*/
class Model {

constructor() {
this.currentTask = new ObservableData();
// 'allTasks' - holds the task objects (title, key, {tabs})
this.allTasks = new ObservableData();
// There is only an initTaskList passed in when 'testing' == true;
constructor(testing, initTaskList) {
// TODO -> read from DB on construction

// Load some testing data in lieu of loading from DB
if (testing) {
this.resources = new Resources(initTaskList);
} else {
this.resources = new Resources([]);
}
// Lol you better be testing because otherwise resources is always empty...

// TODO -> read from DB on initial load
// THIS IS TEMPORARY

this.allTasks.updateData(getTestTaskListSmall());
// binding the 'this' of that function to be the Model reference.
this.registerFinalState = this.registerFinalState.bind(this);
}

// TODO -> Destructor (save changes to the file)

// returns all the tasks that exist.
getAllTasks() {
// return this.allTasks;
// dummy data for testing purposes
return this.allTasks.data;
}
// TODO -> maybe the TaskList should just subscribe, and circumvent this little thing?
/*
* 'desired_resource' - a string (ie. "task_title_list") that indicates which resource the subscriber wants to
* know about.
*/
subscribeTo(obs, desired_resource) {

getAllTaskTitles() {
return this.allTasks.data.map((task) => {
return task.title;
});
switch (desired_resource) {
case "title_key_list":
this.resources.titleKeyList.subscribe(obs);
break;
case "current_task":
this.resources.currentTask.subscribe(obs);
break;
default:
throw Error("hmmm, couldn't find that resource: ${desired_resource}");
}

}

/*
* This is the function that applies changes to the data.
*
* all 'action' is an object with {code: 'what_to_do', args: { } }.
* Unsubscribes the 'obs' from the 'subbed_resource'.
*/
applyChange(action) {
//remove task
//add task
//task completed
//edit task (meaning edit tabs)
}
}
unsubscribeFrom(obs, subbed_resource) {

export default Model;
switch (subbed_resource) {
case "title_key_list":
this.resources.titleKeyList.unsubscribe(obs);
break;
case "current_task":
this.resources.currentTask.unsubscribe(obs);
break;
}

/*
* Maybe the components should only recieve the necessary subscribe function, as opposed to the entire model?
*
* Would it be cleaner to parse the general task[] in the model itself (as various OD's), or have the components just clean
* up whatever they need?
*/
}

/*
* ALTERNATIVE DESIGN: as things are constructed, they are added (similar to the subscriber approach) into an array,
* that lives inside the model, and button clicks / interactions correspond to various actions that the model coordinates
* entirely (components are just conduits, they no longer contain logic).
*
* -- or maybe this is just how clicks should be handled? YES THIS
*
* Maybe something more functional in design? - but I don't really want to pass state all over the place...
*/
/*
* This is the function that updates tasks based on the final state of the tab....
*/
registerFinalState(componentName, finalState, key) {
// Filter for the task that got changed by matching 'key's.
this.resources.allTasks.apply((obsTask) => {
const task = obsTask.getData();
if (task.key === key) {
// finding th tab to update
var foundIt = false;
task.tabs.apply((tab) => {
if (tab.title === componentName) {
tab.info = finalState;
// mark that we found the tab we were looking for
foundIt = true;
}
});
// handling the case when we don't find it
if (!foundIt) {
throw Error(
"You tried to update a component that isn't associated with that task: \n\tcom -> ${componentName}\n\ttask -> ${task})"
);
} else {
obsTask.updateData(task);
}
}
});
}
}

export default Model;
26 changes: 16 additions & 10 deletions src/Model/Model.test.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import Model from './Model'
import { getTestTaskListSmall, getTestTaskListTitlesSmall } from '../TestResources/testutils.js'
import Model from './Model';
import { getTestTaskListSmall, getTestTitleKeyListSmall } from '../TestResources/testutils.js';

describe('Model', () => {
it('constructs a new Model', () => {
const model = new Model();
expect(model !== null);
const model = new Model(true, getTestTaskListSmall());
expect(model.resources !== null);
});

it('returns list of titles on \'getAllTaskTitles()\'', () => {
const model = new Model();
model.allTasks.updateData(getTestTaskListSmall());
it('allows subscription and updates on task title list', () => {
const testing = true;
const model = new Model(testing, getTestTaskListSmall());

const titles = model.getAllTaskTitles();
const correctTitles = getTestTaskListTitlesSmall();
var obs = {
taskTitles: [],
onChange: (newTitles) => {
obs.taskTitles = newTitles;
}
};

expect(titles).toEqual(correctTitles)
model.subscribeTo(obs, "title_key_list");

expect(obs.taskTitles).toEqual(getTestTitleKeyListSmall());
});

});
32 changes: 26 additions & 6 deletions src/Model/ObservableData.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,57 @@
* Objects can 'subscribe' to the data, and are notified (through calls to their 'onChange' method)
* of any updates to the data.
*
* In this application the TaskList will subscribe to the list of task titles as well as the current task,
* and the TaskDisplay will subscribe to the 'currentTask.tabs' property.
* In this application the TaskList will subscribe to the list of task titles as well as the current task
* (in case it needs to color it), and the TaskDisplay will subscribe to the 'currentTask.tabs' property, and pass
* the relevant data into whichever task is loaded.
*/

class ObservableData {

constructor() {
// 'data' - the currently selected task
this.data = null;
// 'subscribers' the objects that want to know about changes to that data
this.subscribers = [];
}

// Add the 'observer' to the subscriber list
subscribe(observer) {
if (typeof(observer.onChange) == "function") {
this.subscribers.push(observer);
observer.onChange(this.data);
observer.onChange(this.copyData());
} else {
// This string template ${thing} isn't working like I think it does...
throw TypeError("${observer} doesn't implement 'onChange() method...'");
throw TypeError("{observer.toString()} doesn't implement 'onChange() method...'");
}
}

// Remove the 'observer' from the subscriber list
unsubscribe(observer) {
this.subscribers = this.subscribers.filter((value) => {
return value !== observer;
});
}

// this returns a shallow copy of the original data
getData() {
return this.copyData();
}
copyData() {
if (this.data === null) {
throw Error("Data has not been set yet");
} else {
if (this.data === 'object') {
return Object.assign({}, this.data);
} else {
return this.data;
}
}
}

// This is the only function that should ever actually change data
updateData(newData) {
this.data = newData;
this.subscribers.forEach((obs) => obs.onChange(newData));
this.subscribers.forEach((obs) => obs.onChange(this.copyData()));
}
}

Expand Down
Loading