37 | `;
38 |
39 | // Find all the buttons in the list and when they are clicked, we dispatch a
40 | // `clearItem` action which we pass the current item's index to
41 | self.element.querySelectorAll('button').forEach((button, index) => {
42 | button.addEventListener('click', () => {
43 | store.dispatch('clearItem', { index });
44 | });
45 | });
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/src/js/components/status.js:
--------------------------------------------------------------------------------
1 | import Component from '../lib/component.js';
2 | import store from '../store/index.js';
3 |
4 | export default class Status extends Component {
5 | constructor() {
6 | super({
7 | store,
8 | element: document.querySelector('.js-status')
9 | });
10 | }
11 |
12 | /**
13 | * React to state changes and render the component's HTML
14 | *
15 | * @returns {void}
16 | */
17 | render() {
18 | let self = this;
19 | let suffix = store.state.items.length !== 1 ? 's' : '';
20 |
21 | self.element.innerHTML = `${store.state.items.length} item${suffix}`;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/js/lib/component.js:
--------------------------------------------------------------------------------
1 | // We're importing the store Class here so we can test against it in the constructor
2 | import Store from '../store/store.js';
3 |
4 | export default class Component {
5 | constructor(props = {}) {
6 | let self = this;
7 |
8 | // We're setting a render function as the one set by whatever inherits this base
9 | // class or setting it to an empty by default. This is so nothing breaks if someone
10 | // forgets to set it.
11 | this.render = this.render || function() {};
12 |
13 | // If there's a store passed in, subscribe to the state change
14 | if(props.store instanceof Store) {
15 | props.store.events.subscribe('stateChange', () => self.render());
16 | }
17 |
18 | // Store the HTML element to attach the render to if set
19 | if(props.hasOwnProperty('element')) {
20 | this.element = props.element;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/js/lib/pubsub.js:
--------------------------------------------------------------------------------
1 | export default class PubSub {
2 | constructor() {
3 | this.events = {};
4 | }
5 |
6 | /**
7 | * Either create a new event instance for passed `event` name
8 | * or push a new callback into the existing collection
9 | *
10 | * @param {string} event
11 | * @param {function} callback
12 | * @returns {number} A count of callbacks for this event
13 | * @memberof PubSub
14 | */
15 | subscribe(event, callback) {
16 |
17 | let self = this;
18 |
19 | // If there's not already an event with this name set in our collection
20 | // go ahead and create a new one and set it with an empty array, so we don't
21 | // have to type check it later down-the-line
22 | if(!self.events.hasOwnProperty(event)) {
23 | self.events[event] = [];
24 | }
25 |
26 | // We know we've got an array for this event, so push our callback in there with no fuss
27 | return self.events[event].push(callback);
28 | }
29 |
30 | /**
31 | * If the passed event has callbacks attached to it, loop through each one
32 | * and call it
33 | *
34 | * @param {string} event
35 | * @param {object} [data={}]
36 | * @returns {array} The callbacks for this event, or an empty array if no event exits
37 | * @memberof PubSub
38 | */
39 | publish(event, data = {}) {
40 | let self = this;
41 |
42 | // There's no event to publish to, so bail out
43 | if(!self.events.hasOwnProperty(event)) {
44 | return [];
45 | }
46 |
47 | // Get each subscription and call its callback with the passed data
48 | return self.events[event].map(callback => callback(data));
49 | }
50 | }
--------------------------------------------------------------------------------
/src/js/main.js:
--------------------------------------------------------------------------------
1 | import store from './store/index.js';
2 |
3 | // Load up components
4 | import Count from './components/count.js';
5 | import List from './components/list.js';
6 | import Status from './components/status.js';
7 |
8 | // Load up some DOM elements
9 | const formElement = document.querySelector('.js-form');
10 | const inputElement = document.querySelector('#new-item-field');
11 |
12 | // Add a submit event listener to the form and prevent it from posting back
13 | formElement.addEventListener('submit', evt => {
14 | evt.preventDefault();
15 |
16 | // Grab the text value of the textbox and trim any whitespace off it
17 | let value = inputElement.value.trim();
18 |
19 | // If there's some content, trigger the action and clear the field, ready for the next item
20 | if(value.length) {
21 | store.dispatch('addItem', value);
22 | inputElement.value = '';
23 | inputElement.focus();
24 | }
25 | });
26 |
27 | // Instantiate components
28 | const countInstance = new Count();
29 | const listInstance = new List();
30 | const statusInstance = new Status();
31 |
32 | // Initial renders
33 | countInstance.render();
34 | listInstance.render();
35 | statusInstance.render();
36 |
--------------------------------------------------------------------------------
/src/js/store/actions.js:
--------------------------------------------------------------------------------
1 | export default {
2 | addItem(context, payload) {
3 | context.commit('addItem', payload);
4 | },
5 | clearItem(context, payload) {
6 | context.commit('clearItem', payload);
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/src/js/store/index.js:
--------------------------------------------------------------------------------
1 | import actions from './actions.js';
2 | import mutations from './mutations.js';
3 | import state from './state.js';
4 | import Store from './store.js';
5 |
6 | export default new Store({
7 | actions,
8 | mutations,
9 | state
10 | });
11 |
--------------------------------------------------------------------------------
/src/js/store/mutations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | addItem(state, payload) {
3 | state.items.push(payload);
4 |
5 | return state;
6 | },
7 | clearItem(state, payload) {
8 | state.items.splice(payload.index, 1);
9 |
10 | return state;
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/src/js/store/state.js:
--------------------------------------------------------------------------------
1 | export default {
2 | items: [
3 | 'I made this',
4 | 'Another thing'
5 | ]
6 | };
7 |
--------------------------------------------------------------------------------
/src/js/store/store.js:
--------------------------------------------------------------------------------
1 | import PubSub from '../lib/pubsub.js';
2 |
3 | export default class Store {
4 | constructor(params) {
5 | let self = this;
6 |
7 | // Add some default objects to hold our actions, mutations and state
8 | self.actions = {};
9 | self.mutations = {};
10 | self.state = {};
11 |
12 | // A status enum to set during actions and mutations
13 | self.status = 'resting';
14 |
15 | // Attach our PubSub module as an `events` element
16 | self.events = new PubSub();
17 |
18 | // Look in the passed params object for actions and mutations
19 | // that might have been passed in
20 | if(params.hasOwnProperty('actions')) {
21 | self.actions = params.actions;
22 | }
23 |
24 | if(params.hasOwnProperty('mutations')) {
25 | self.mutations = params.mutations;
26 | }
27 |
28 | // Set our state to be a Proxy. We are setting the default state by
29 | // checking the params and defaulting to an empty object if no default
30 | // state is passed in
31 | self.state = new Proxy((params.state || {}), {
32 | set: function(state, key, value) {
33 |
34 | // Set the value as we would normally
35 | state[key] = value;
36 |
37 | // Trace out to the console. This will be grouped by the related action
38 | console.log(`stateChange: ${key}: ${value}`);
39 |
40 | // Publish the change event for the components that are listening
41 | self.events.publish('stateChange', self.state);
42 |
43 | // Give the user a little telling off if they set a value directly
44 | if(self.status !== 'mutation') {
45 | console.warn(`You should use a mutation to set ${key}`);
46 | }
47 |
48 | // Reset the status ready for the next operation
49 | self.status = 'resting';
50 |
51 | return true;
52 | }
53 | });
54 | }
55 |
56 | /**
57 | * A dispatcher for actions that looks in the actions
58 | * collection and runs the action if it can find it
59 | *
60 | * @param {string} actionKey
61 | * @param {mixed} payload
62 | * @returns {boolean}
63 | * @memberof Store
64 | */
65 | dispatch(actionKey, payload) {
66 |
67 | let self = this;
68 |
69 | // Run a quick check to see if the action actually exists
70 | // before we try to run it
71 | if(typeof self.actions[actionKey] !== 'function') {
72 | console.error(`Action "${actionKey} doesn't exist.`);
73 | return false;
74 | }
75 |
76 | // Create a console group which will contain the logs from our Proxy etc
77 | console.groupCollapsed(`ACTION: ${actionKey}`);
78 |
79 | // Let anything that's watching the status know that we're dispatching an action
80 | self.status = 'action';
81 |
82 | // Actually call the action and pass it the Store context and whatever payload was passed
83 | self.actions[actionKey](self, payload);
84 |
85 | // Close our console group to keep things nice and neat
86 | console.groupEnd();
87 |
88 | return true;
89 | }
90 |
91 | /**
92 | * Look for a mutation and modify the state object
93 | * if that mutation exists by calling it
94 | *
95 | * @param {string} mutationKey
96 | * @param {mixed} payload
97 | * @returns {boolean}
98 | * @memberof Store
99 | */
100 | commit(mutationKey, payload) {
101 | let self = this;
102 |
103 | // Run a quick check to see if this mutation actually exists
104 | // before trying to run it
105 | if(typeof self.mutations[mutationKey] !== 'function') {
106 | console.log(`Mutation "${mutationKey}" doesn't exist`);
107 | return false;
108 | }
109 |
110 | // Let anything that's watching the status know that we're mutating state
111 | self.status = 'mutation';
112 |
113 | // Get a new version of the state by running the mutation and storing the result of it
114 | let newState = self.mutations[mutationKey](self.state, payload);
115 |
116 | // Merge the old and new together to create a new state and set it
117 | self.state = Object.assign(self.state, newState);
118 |
119 | return true;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------