Product
Socket Now Supports uv.lock Files
Socket now supports uv.lock files to ensure consistent, secure dependency resolution for Python projects and enhance supply chain security.
Immutable MVVM for React
IMVVM is designed to complement the React library. A Facebook and Instagram collaboration.
React is:
A Javascript Library for building User Interfaces
To further quote the React website, "lots of people use React as the 'V' in MVC". Well I thought, why not use React as the 'V' in MVVM, and keeping inline with React's philosophy on immutability, lets make it use immutable data. So I created IMVVM.
This is my take on MVVM and immutability using javascript. IMVVM tries to resemble a React application. So it not only feels like your developing in the same environment, but if your coming from React, you should be able to pick up IMVVM quickly.
Anyway, download it, run the example application and try it out. Maybe avoid production environments for the moment. Be sure to raise issues in the Issues Register as you encounter them and please feel free to create pull requests.
Thanks.
TODO: Write some tests. I know, they should have already been done...
IMVVM can be loaded as:
IMVVM
is exposed as a global variable <script src="imvvm.min.js"></script>
$ npm install imvvm
$ bower install imvvm
require(['./imvvm.min.js'], function (IMVVM) {
// Do something with IMVVM
});
var PersonModel = IMVVM.createModel({
getInitialState: function(){
return {
age: this.calculateAge(this.dob),
id: this.id ? this.id : this.uuid()
};
},
id: {
get: function(){
return this.state.id;
}
},
name: {
get: function(){
return this.state.firstName;
},
set: function(newValue){
this.setState({'name': newValue});
}
},
dob: {
get: function(){
return this.state.dob;
},
set: function(newValue){
var age;
if(newValue.length === 10){
age = this.calculateAge(newValue);
}
if(newValue.length === 0){
age = 'Enter your Birthday';
}
this.setState({
dob: newValue,
age: age
});
}
},
gender: {
get: function(){
return this.state.gender;
},
set: function(newValue){
this.setState({'gender': newValue});
}
},
age: {
get: function(){
return this.state.age;
}
},
calculateAge: function(dob){
var DOB = new Date(dob);
var ageDate = new Date(Date.now() - DOB.getTime());
var age = Math.abs(ageDate.getFullYear() - 1970);
return Number.isNaN(age) ? 'Enter your Birthday' : age + ' years old';
},
uuid: function () {
var i, random;
var uuid = '';
for (i = 0; i < 32; i++) {
random = Math.random() * 16 | 0;
if (i === 8 || i === 12 || i === 16 || i === 20) {
uuid += '-';
}
uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random))
.toString(16);
}
return uuid;
},
});
#####getInitialState()
The getInitialState
function initializes any fields that require a value on creation. Such values include, default values and calculated fields. This function has access to state that has been supplied during initializtion, and therefore this
will be able to refer to the state.
#####name
name
is a simple field descriptor. It defines a getter and setter. The getter returns state that resides in the state's property of the same name. The setter uses the passed in value to transition to the model to the next state, by passing the value, as an object to the setState function. This will then notify any ViewModels that have registered a stateChangeHandler with this model.
#####age
age
is a calculated field. It is set by setting dob
. dob
uses the calculateAge function to set the value of age and passes both the dob and age values to setState. Please note that age
needed to be initialized in the getInitialState() function.
var PersonsViewModel = IMVVM.createViewModel({
getInitialState: function(){
var nextState = {};
nextState.collection = DataService.getData().map(function(person, idx){
if (idx === 0){
nextState.selectedPerson = this.Person(person, true);
return nextState.selectedPerson;
}
return this.Person(person, true);
}.bind(this));
return nextState;
},
getWatchedState: function() {
return {
'hobbies': {
alias: 'hobbiesContext',
},
'online': {
alias: 'imOnline'
}
};
},
imOnline: {
kind:'pseudo',
get: function(){
return this.state.imOnline;
}
},
selectedPerson: {
kind: 'instance',
get: function() { return this.state.selectedPerson; }
},
collection: {
kind: 'array',
get: function(){ return this.state.collection; },
},
personStateChangedHandler: function(nextState, prevState){
var persons = {};
persons.collection = this.collection.map(function(person){
if(person.id === nextState.id){
persons.selectedPerson = this.Person(nextState);
return persons.selectedPerson;
}
return person;
}.bind(this));
this.setState(persons);
},
Person: function(personState, init){
return new PersonModel(this.personStateChangedHandler)(personState, init);
},
selectedHobby: {
kind: 'pseudo',
get: function() {
return this.state.hobbiesContext.current ? this.state.hobbiesContext.current.name: void(0);
}
},
selectPerson: function(id){
for (var i = this.collection.length - 1; i >= 0; i--) {
if(this.selectedPerson.id !== id && this.collection[i].id === id){
this.setState({ selectedPerson: this.collection[i] });
break;
}
}
},
addPerson: function(value){
var nextState = {};
var name;
if(value && value.length > 0){
name = value.split(' ');
nextState.selectedPerson = this.Person({
firstName: name[0],
lastName: name.slice(1).join(' ')
}, true);
nextState.collection = this.collection.slice(0);
nextState.collection = nextState.collection.concat(nextState.selectedPerson);
this.setState(nextState);
}
},
deletePerson: function(uid){
var nextState = {};
nextState.collection = this.collection.filter(function(person){
return person.id !== uid;
});
nextState.selectedPerson = void(0);
if(nextState.collection.length > 0){
if (this.selectedPerson.id === uid){
nextState.selectedPerson = this.Person(nextState.collection[0]);
} else {
nextState.selectedPerson = this.Person(this.selectedPerson);
}
}
this.setState(nextState);
},
});
#####getInitialState()
The getInitialState
function initializes any fields that require a value and
initialize models to establish the data for the Viewmodel. Unlike Models, the ViewModel does not have access to state
.
#####getWatchedState()
The getWatchedState
function establises links between ViewModels. What is happening in this instance, is that we are watching the hobbies data context and refering to it, within this viewModel, with the alias of hobbiesContext. So we are then able to refer to is from teh state object as this.state.hobbiesContext...
. Enabling us to display fields from other ViewModels and also update thier values.
We can also reference fields from the DomainViewModel. We simply specify the field name and supply an alias. Then it can be refered to within the state object.
#####imOnline
The imOnline
field is referencing the linked DomainModel field online
with the alias of imOnline
. It is simply forwarding the online value it recieves from the DomainViewModel to the View and has no influence over its output. It is for this reason that the field descriptor has a kind
decorator of pseudo
. Any field that obtains its value from elsewhere, should be flagged with the decorator kind:'pseudo'
, unless it is an instance
or array
.
#####selectedPerson
selectedPerson
is another field that has a kind
decorator. The selectedPerson
field is of kind instance
. Which simply means that it will return a model instance.
#####collection
collection
has a kind
decorator of array
. Yes, you guessed it, because it returns an array.
The reason that the kindd decorator is used is because IMVVM does some extra processing with these types.
#####personStateChangedHandler
personStateChangedHandler
will be invoked anytime setState
is called within the model that it is registered with. This function processes the change in the ViewModel and notifies the DomainModel so that the DomainModel can transition to the next state.
#####Person
Person
is a wrapper around the PersonModel
constructor. It makes PersonModel
easier to invoke and acts like a factory of sorts. The parameters passed to Person
are passed directly to the PersonModel
constructor, but not before registering the stateChangedHandler personStateChangedHandler
, to ensure that all state changes in PersonModel and handled approapriately.
#####selectPerson, addPerson & deletePerson
These functions are exposed to the View and enaable tasks to be performed in the ViewModel. However, most interactions will occur via the selectedPerson
which exposes the model instance to the View.
I've added a snippet of HobbiesViewModel, from the example application to explain a little more about getWatchedState
.
HobbiesViewModel snippet
var HobbiesViewModel = IMVVM.createViewModel({
getWatchedState: function() {
return {
'persons': {
alias: 'personsContext',
fields: {
'selectedPerson': this.onPersonChangedHandler
}
},
'busy': {
alias: 'busy'
}
};
},
onPersonChangedHandler: function(nextState, prevState, field, context){
if(this.current !== void(0) && context === 'persons' &&
nextState.id !== prevState.id){
return { hobbies: { current: void(0) }, busy: false };
}
},
...
selectHobby: function(id){
for (var i = this.hobbies.length - 1; i >= 0; i--) {
if ((this.current === void(0) || this.current.id !== id) && this.hobbies[i].id === id){
this.setState({current: this.Hobby(this.hobbies[i])}, {busy: true});
/*
//OR use a callback
this.setState({current: this.Hobby(this.hobbies[i])}, function(){
this.setState(void(0), {busy: true});
}.bind(this));
*/
break;
}
}
},
...
});
#####getWatchedState()
Notice in this instance of the getWatchedState
function the persons
object has an extra property called fields. This specifies that we are not only linking to the persons
data context, but we would also like to be notified when persons.selectedPerson
changes, and if it does change trigger the onPersonChanged
handler.
#####onPersonChangedHandler
onPersonChangedHandler
will be triggered whenever any of the fields that it is assigned to changes. It is important to note that, if there is a change a state object is returned. If there is no change, return either void(0) or don't return anything. It should also be noted that the returned state object has a property with the data context, hobbies
, and the associated object next state. This is necessary so that any data context or domain property can be updated from this ViewModel by simply specfying it in the returned object.
#####selectHobby
The selectHobby
function is nothing special. What is different about it is the 'setState' function. ViewModels get an extra parameter, which enables the ViewModel to update state in other data contexts. The second parameter takes a state object, not dissimilar to the state object that is returned from onPersonChangedHandler
. The second parameter accepts an object that specifies the data context\domain property and associated state.
For instance this.setState({current: this.Hobby(this.hobbies[i])}, {busy: true});
. The first parameter is the next state for hobbies
data context, the second parameter specifies that busy
, in the domain data context should be changed to true
. This second parameter also accepts, for example, {persons: {selectedPerson: this.Person(personState)}}
.
Also noted in the comments is that this can be achieved with a callback, ensuring to pass void(0)
as the first parameter to setState
.
n.b. You may have noticed that not all fields have setters, however, the values are still able to be updated. This is what setState
does. You only need to supply a setter if you would like the View to update the field directly.
var DomainViewModel = IMVVM.createDomainViewModel({
getInitialState: function(){
return {
online: true,
busy: false
};
},
persons: {
viewModel: PersonsViewModel,
get: function(){
return this.state.persons;
}
},
hobbies: {
viewModel: HobbiesViewModel,
get: function(){
return this.state.hobbies;
}
},
personCount: {
kind:'pseudo',
get: function(){
return this.persons ? this.persons.collection.length : 0;
}
},
busy: {
get: function(){
return this.state.busy;
},
},
online: {
get: function(){
return this.state.online;
},
set: function(newValue){
this.setState({'online': newValue });
}
},
});
#####getInitialState()
The 'getInitialState' is the same as for the ViewModel.
#####persons
The persons
property is setting up a data context called 'persons'. It has a special decorator called viewModel
which specifies which ViewModel is associated to this data context.
Once you have created your Models. ViewModels and DomainViewModel, you're ready to hook it up the the View. All you need to do is specify the mixin and IMVVM will attach a domainDataContext
to the state object that will be kept in sync with you're ViewModel.
var ApplicationView = React.createClass({
mixins: [IMVVM.mixin],
render: function(){
return (
<div>
<NavBarView appContext={this.state.domainDataContext} />
<SideBarView appContext={this.state.domainDataContext} />
<DetailsView appContext={this.state.domainDataContext} />
</div>
);
}
});
The domainDataContext
has all the properties that were specified in the DomainViewModel. So this domainDataContext
will have the following properties:
Then you just render the View, specifying a domainModel
prop, that references the 'DomainViewModel'.
React.renderComponent(<ApplicationView domainModel={DomainViewModel}/>,
document.getElementById('container'));
A word on how to use the exposed Model -> selectedPerson
As mentioned above, the PersonsViewModel
has a persons
data context. This data context exposes a Person
model instance via the selectedPerson
property.
To update the Person
instance, simply assign values to the associated properties on the selectedPerson
property.
FormView example
var FormView = React.createClass({
updateName: function(e){
this.props.appContext.persons.selectedPerson.name = e.target.value;
},
updateGender: function(e){
this.props.appContext.persons.selectedPerson.gender = e.target.value;
},
updateDOB: function(e){
this.props.appContext.persons.selectedPerson.dob = e.target.value;
},
updateDOB: function(e){
this.props.appContext.persons.selectedPerson.dob = e.target.value;
},
render: function() {
var current = this.props.appContext.persons.selectedPerson;
return (
<div key={current.id}>
<form role="form">
<div>
<label>Name</label>
<div>
<input type="text" value={current.name}
onChange={this.updateName} />
</div>
</div>
<div>
<label>Gender</label>
<div>
<div>
<label>
<input type="radio" onChange={this.updateGender}
value="male"
checked={current.gender === 'male'} />
Male
</label>
</div>
<div>
<label>
<input type="radio" onChange={this.updateGender}
value="female"
checked={current.gender === 'female'} />
Female
</label>
</div>
</div>
</div>
<div>
<label>Birthday</label>
<div>
<input type="text"
placeholder="yyyy-mm-dd"
value={current.dob}
onChange={this.updateDOB} />
</div>
</div>
<div className="form-group">
<label>Age</label>
<div>
<div>
{current.age}
</div>
</div>
</div>
</form>
</div>
);
}
});
##Running the example application
The example application is a good starting place when figuring out how things work. So to get it running, navigate to the ./example
directory and run the following commands.
$ npm install
$ cd app
$ bower install
$ bower install imvvm
$ cd ..
$ grunt serve
##API
###Class ####Constructors
#####function createDomainViewModel(object specification) Creates a DomainViewModel object.
parameters
specification - see Specification
#####function createViewModel(object specification) Creates a ViewModel object.
parameters
specification - see Specification
#####function createModel(object specification) Creates a Model object.
parameters
specification - see Specification
###Instance ####Functions
#####void setState(object nextState[, oject nextDomainState, function callback]) Transition Data Context to the next state.
parameters
nextState
Object containing the next state values.
nextDomainState n.b. This parameter is only available in ViewModels
Object containing the next state values which are to be passed to the Domain Data Context.
example
//From PersonsViewModel
{
hobbies: { current: void(0) },
busy: false
}
callback
Used to do sequential setState calls.
Available in: DomainViewModel, ViewModel, Model
#####object extend(object currentState[, object... nextState]) Creates a shallow copy of currentState. Adds/replaces properties with properties of subsequent objects.
parameters
currentState
Old state object.
nextState
Next state objects.
Available in: DomainViewModel, ViewModel, Model
####Properties
#####state Holds object state.
Available in: DomainViewModel, ViewModel, Model
#####previousState Holds Domain Data Context previous state.
Available in: DomainViewModel
###Specification ####Functions
#####object getInitialState() Called when initalized in order to initialize state. Returns a state object;
Available in: DomainViewModel, ViewModel, Model
Optional: true
#####object getWatchedState() Provides watched items to domain. Returns a WatchedState object definition.
######Watching ViewModel state dataContext: { [alias]: preferredName, [fields]: { dataContextField: ViewModelStateChangedHandler } }
######Watching DomainViewModel state field: { alias: preferredName }
example
getWatchedState: function() {
return {
'persons': {
alias: 'personsContext',
fields: {
'selected': this.onPersonChange
}
},
'busy': {
alias: 'busy'
}
}
},
Available in: ViewModel
Optional: true
#####object ViewModelStateChangedHandler(object nextState, object previousState, string field, string dataContext)
arguments
nextState
Next state of the watched field.
previousState
Previous state of the watched field.
field
Watched field name.
dataContext
Watched Data Context name.
example
onPersonChange: function(nextState, prevState, field, context){
if(this.current !== void(0) && context === 'persons' &&
nextState.id !== prevState.id){
return { hobbies: { current: void(0) }, busy: false };
}
}
Available in: ViewModel
#####void ModelStateChangedHandler(object nextState,object previousState[, function callback])
arguments
nextState
Next state of the model.
previousState
Previous state of the model.
callback
personStateChangedHandler: function(nextState, prevState){
var persons = {};
persons.collection = this.collection.map(function(person){
if(person.id === nextState.id){
persons.selected = this.Person(nextState, person, true);
return persons.selected;
}
return person;
}.bind(this));
this.setState(persons);
},
Available in: ViewModel
#####ModelInstance ModelName([function stateChangedHandler])([object nextState, boolean initialize]) Returns a new model instance. This function is usually wrapped in another function for easier usage.
parameters
stateChangedHandler
State changed handler which is triggered whenever setState is called from within the model.
nextState
Object containing the next state values for the model.
initialize
Boolean value indicating the model instance should be initialized. (Calls getInitialState())
Available in: ViewModel, Model
Usage example
n.b. This is suggested usage and not part of the API
######ModelInstance ModelFactory(object nextState[, boolean initialize:false])
Person: function(person, init){
return new PersonModel(this.personStateChangedHandler)(person, init);
},
####Field
#####fieldName : Descriptor Available in: DomainViewModel, ViewModel, Model
####Descriptor
#####Functions ######get()
Available in: DomainViewModel, ViewModel, Model
######void set(newValue)
Available in: DomainViewModel, ViewModel, Model
#####Decorators
######kind: ['instance' || 'array' || 'pseudo'] instance
Specifies that the getter returns a model instance.
array
Specifies that the field holds an array.
pseudo
Specifies that the value for this field is obtained from other properties.
Available in: DomainViewModel, ViewModel, Model
######viewModel: ViewModel Class viewModel
Identifies the ViewModel to be used for the defined data context.
Available in: DomainViewModel
###Mixin ####mixin
#####mixins: [IMVVM.mixin]
Adds domainDataContext to state object.
var ApplicationView = React.createClass({
mixins: [IMVVM.mixin],
render: function(){
return <DetailsView appContext={this.state.domainDataContext} />;
}
});
####props #####domainModel= DomainViewModel Class #####[enableUndo]= boolean: default = false
Point the View to the DomainViewModel.
React.renderComponent(<ApplicationView domainModel={DomainViewModel}/>,
document.getElementById('container'));
Most ECMAScript 5 compliant browsers. IE8 and below are not supported.
Entrendipity - Follow @entrendipity
Frank Panetta - Follow @fattenap
##License ###The MIT License (MIT)
Copyright (c) 2014 Entrendipity
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
FAQs
Immutable MVVM for React - Renamed: Please use Astarisx
The npm package imvvm receives a total of 5 weekly downloads. As such, imvvm popularity was classified as not popular.
We found that imvvm demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Product
Socket now supports uv.lock files to ensure consistent, secure dependency resolution for Python projects and enhance supply chain security.
Research
Security News
Socket researchers have discovered multiple malicious npm packages targeting Solana private keys, abusing Gmail to exfiltrate the data and drain Solana wallets.
Security News
PEP 770 proposes adding SBOM support to Python packages to improve transparency and catch hidden non-Python dependencies that security tools often miss.