On Saturday, December 18, 2021 at 2:14:27 PM UTC-6, Michael Haufe (TNO) wrote:
On Thursday, December 16, 2021 at 11:18:18 PM UTC-6, [email protected] wrote:
I've read through Michael Haufe's example a few more times, and
re-read the old article by Peter Michaux. And through all of it, I like
the idea of structuring stuff. The major divisions of responsibility
make sense. And to a certain extent, the lines of communication
make sense. But in all of these implementations there feels (to me)
like a whole lotta stuff I don't need. Or don't need yet. Or don't understand. Or something.
Most likely "don't need yet". MVC is overkill for simplistic pages or things don't really rise to the level of an application.
Recall that Peter Michaux and I both presented an MVC library in ~100 lines. With such examples they should be treated as impressionistic and incomplete.
So I've tried to build everything up as simply as I could manage.
I don't need to dynamically change the list of observers beyond
adding at least one: so it's just one one-liner function; etc. I did run
in to one weird corner in trying to make a Button out of a View.
It doesn't really need a model. But I have to give it some kind of
model because the constructor needs to add itself as an observer
to *something*.
You don't need a model. It could be just the Controller and View.
Hmm. I don't quite follow that. Conceptually yes, but the way I've
implemented it, the constructor for the View superclass expects
a Model (something that implements observedBy()). But I tucked it
away inside the Button constructor now.
But this feels like progress on the major front. It's a little sloppy
and free-for-all inside the objects, but everything is *inside* the objects! This code doesn't actually draw anything except the
buttons. And the buttons don't visibly do anything. But importantly
they do all of this nothing *without errors*.
I'm glad you've made progress with my back-of-napkin example.
There are plenty of places that could be improved with naming
conventions, refactoring, utilities, etc.
One thing I'd consider is to align the obervers with the DOM.
Since the DOM uses `addEventListener` is might be more
clear to rename `observedBy` to something similar.
Worth considering. I like the "adjective" name as being more functional
than a "verb" name, but maybe that's not the most important factor here.
`container` might be more intuitive being renamed to `parent`
Yeah, that's fewer letters too.
Should the Notes be responsible for toggling? Would that make more sense in the owning controller?
Hmm. Maybe. The click event that initiates it should probably route
through the controller. But I strongly see it as an operation on the model which should be part of the model's interface.
Again on the event management side. Note that the DOM's 'addEventListener' can accept an object as its second parameter.
This could be a good opportunity to simplify all of the event management in general
An example:
[snip interesting event handling example]
I finished fleshing out the code to the same level of functionality as the previous
It relies on the scales and running_sum() functions from my other thread in a file called fim.js. Plus an additional scale
const pentatonic = [ 2, 3, 2, 2, 3 ]; // c#..d#..f#..g#..a#..(c#)
used for painting the black keys on the keyboard.
The way I have it set up, the PianorollView builds all the functions
that call model.toggle(). And the View doesn't have knowledge of the Controller, but it does have a pointer to the Model. The Controller
includes the others by composition rather than inheritance.
<html><!-- fim.html -->
<head>
<meta charset="utf-8" />
<style>
.pianoroll table { table-layout: fixed;
border-collapse: collapse;
border: 1px solid;
cursor: pointer;
}
.pianoroll td { border: 1px solid;
padding: 0; }
.pianoroll tr.heavy td.black:hover { color: red; background-color: red; }
.pianoroll td.black:hover { color: blue; background-color: blue; }
.pianoroll td:hover { color: red; background-color: red; }
.heavy { border: 2.5px solid; }
.black { background-color: black; }
</style>
</head>
<body>
</body>
<script src="fim.js"></script> <!--scales and chords-->
<script src="mvc.js"></script> <!--mvc piano roll-->
</html>
// mvc.js
class Model {
constructor(){ this._observers = []; }
observedBy( observer ){ this._observers.push( observer ); return this; }
notify( event ){ this._observers.forEach(o=>o.update(event)); }
}
class View {
constructor( model, container ){
this._model = model.observedBy( this );
this._container = container;
this._root = this.initRoot();
}
initRoot(){ return document.createElement("div"); }
render(){ this._container.appendChild( this._root ); }
update( event ){}
}
class Controller {
constructor(){ }
}
class Notes extends Model {
constructor(){
super();
this.data = [];
}
sync(){ this.notify({data:this.data}); }
clear(){
this.data = [];
this.sync();
}
add( notes ){
this.data.push(notes);
this.sync();
}
toggle( y, x ){
if( this.data[ y ].includes( x ) ){
this.data[ y ] = this.data[ y ].filter( n=> n!=x );
} else {
this.data[ y ].push( x );
}
this.sync();
}
}
class PianoRollView extends View {
constructor( model, container ){
super( model, container );
}
initRoot(){ return document.createElement("table"); }
update( event ){ this.draw( event.data ); }
draw( notes ){
var table = document.createElement("table");
var row = this.row;
var cell = this.cell;
var black = this.black;
var toggle = this.toggle;
var model = this._model;
notes.forEach( function( n, idx ){
table.appendChild( row( n, idx, toggle, model, cell, black ) );
});
table.appendChild( this.keyboard() );
this._container.removeChild( this._root );
this._root = table;
this.render();
}
toggle( model, i, j ){
return ()=> model.toggle( i, j );
}
add( model, i, j ){
return ()=> model.add( [j] );
}
keyboard(){
return this.heavy( this.row( running_sum( repeat( octaves, pentatonic ) )
.map( x=> x+1 ),
0, this.add, this._model, this.cell, this.black ) );
}
row( pitches, idx, action, model, cell, black ){
pitches.sort( (x,y)=> x<y?-1 :x==y?0 :1 );
var tr = document.createElement("tr");
var i = 0;
pitches.forEach( p=>{
for( ; i < p && i < octaves*12; i++ )
tr.appendChild( cell( action, model, idx, i ) );
if( i >= octaves*12 ) return;
tr.appendChild( black( cell( action, model, idx, i++ ) ) );
});
for( ; i < octaves*12; i++ )
tr.appendChild( cell( action, model, idx, i ) );
return tr;
}
black( thing ){ thing.className = 'black'; return thing; }
heavy( thing ){ thing.className = 'heavy'; return thing; }
cell( action, model, i, j ){
var td = document.createElement("td");
var a = document.createElement("a");
td.appendChild( a );
var content = document.createTextNode("\u00A0"); //nbsp
a.appendChild( content );
a.onclick = action( model, i, j );
return td;
}
}
class PianoPlayer extends View {
constructor( model, container ){
super( model, container );
}
play( notes ){ }
}
class Button extends View {
constructor( label, action, container ){
super( new Model(), container );
this._root.textContent = label;
this._root.onclick = action;
this.render();
}
initRoot(){ return document.createElement("button"); }
}
class PianoRoll extends Controller {
constructor( container = document.body ){
super();
var a = document.createElement("div");
a.className = 'pianoroll';
container.appendChild(a);
this.model = new Notes();
this.view = new PianoRollView( this.model, a );
this.view.render();
this.model.sync();
this.player = new PianoPlayer( this.model, a );
var b = document.createElement("div");
container.appendChild(b);
var model = this.model;
var player = this.player;
this.add = new Button( 'add row', ()=>model.add([]), b );
this.play = new Button( 'play', ()=>player.play(model.data), b );
this.clear = new Button( 'clear', ()=>model.clear(), b );
}
}
const octaves = 4;
const piano = new PianoRoll();
--- SoupGate-Win32 v1.05
* Origin: fsxNet Usenet Gateway (21:1/5)