diff --git a/book/snippets/01-JavaScript-Design-Patterns/0-design-pattern-categorization.es2015.js b/book/snippets/01-JavaScript-Design-Patterns/0-design-pattern-categorization.es2015.js new file mode 100644 index 00000000..da37c71e --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/0-design-pattern-categorization.es2015.js @@ -0,0 +1,40 @@ +//*******************************************************// +// A brief note on classes +//*******************************************************// + +// Section contains description of ES2015, but not use it. +// I suggest remove the description and put the new examples. + +//********************** Snippet 1 **********************// + +// A car "class" + +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance +// [ES2015+] We used new template literals for string interpolation +class Car { + constructor(model) { + this.model = model; + this.color = 'silver'; + this.year = '2012'; + } + + getInfo() { + return `${this.model} ${this.year}`; + } +} + +//********************** Snippet 2 **********************// + +// Usage: + +// [ES2015+] We used new keyword const for immutable constant declaration +const myCar = new Car('ford'); + +myCar.year = '2010'; + +console.log(myCar.getInfo()); + +// Here the link on Stoyan Stefanov's post, it's a good post. +// But more modern data can be obtained here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes diff --git a/book/snippets/01-JavaScript-Design-Patterns/0-design-pattern-categorization.es5.js b/book/snippets/01-JavaScript-Design-Patterns/0-design-pattern-categorization.es5.js new file mode 100644 index 00000000..23827f74 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/0-design-pattern-categorization.es5.js @@ -0,0 +1,26 @@ +//*******************************************************// +// A brief note on classes +//*******************************************************// + +//********************** Snippet 1 **********************// + +// A car "class" +function Car( model ) { + + this.model = model; + this.color = "silver"; + this.year = "2012"; + + this.getInfo = function () { + return this.model + " " + this.year; + }; + +} + +//********************** Snippet 2 **********************// + +var myCar = new Car("ford"); + +myCar.year = "2010"; + +console.log( myCar.getInfo() ); \ No newline at end of file diff --git a/book/snippets/01-JavaScript-Design-Patterns/1-the-constructor-pattern.es2015.js b/book/snippets/01-JavaScript-Design-Patterns/1-the-constructor-pattern.es2015.js new file mode 100644 index 00000000..8e8bf7d3 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/1-the-constructor-pattern.es2015.js @@ -0,0 +1,179 @@ +//*******************************************************// +// Object Creation +//*******************************************************// + +//********************** Snippet 1 **********************// + +// [ES2015+] We used new keyword const for immutable constant declaration +// Each of the following options will create a new empty object: + +const newObject = {}; + +// or +const newObject = Object.create(Object.prototype); + +// or +const newObject = new Object(); + +//********************** Snippet 2 **********************// + +// ECMAScript 3 compatible approaches + +// 1. Dot syntax + +// Set properties +newObject.someKey = 'Hello World'; + +// Get properties +// [ES2015+] We used new keyword const for immutable constant declaration +const value = newObject.someKey; + +// 2. Square bracket syntax + +// Set properties +newObject['Some Key'] = 'Hello World'; + +// Get properties +// [ES2015+] We used new keyword const for immutable constant declaration +const value = newObject['Some Key']; + +// ECMAScript 5 only compatible approaches +// For more information see: http://kangax.github.com/es5-compat-table/ + +// 3. Object.defineProperty + +// Set properties +Object.defineProperty(newObject, 'someKey', { + value: "for more control of the property's behavior", + writable: true, + enumerable: true, + configurable: true, +}); + +// If the above feels a little difficult to read, a short-hand could +// be written as follows: +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function expression syntax +const defineProp = (obj, key, value) => { + const config = { + value: value, + writable: true, + enumerable: true, + configurable: true, + }; + Object.defineProperty(obj, key, config); +}; + +// To use, we then create a new empty "person" object +// [ES2015+] We used new keyword const for immutable constant declaration +const person = Object.create(Object.prototype); + +// Populate the object with properties +defineProp(person, 'car', 'Delorean'); +defineProp(person, 'dateOfBirth', '1981'); +defineProp(person, 'hasBeard', false); + +console.log(person); +// Outputs: Object {car: "Delorean", dateOfBirth: "1981", hasBeard: false} + +// 4. Object.defineProperties + +// Set properties +Object.defineProperties(newObject, { + someKey: { + value: 'Hello World', + writable: true, + }, + + anotherKey: { + value: 'Foo bar', + writable: false, + }, +}); + +// Getting properties for 3. and 4. can be done using any of the +// options in 1. and 2. + +//********************** Snippet 3 **********************// + +// Usage: + +// Create a race car driver that inherits from the person object +// [ES2015+] We used new keyword const for immutable constant declaration +const driver = Object.create(person); + +// Set some properties for the driver +defineProp(driver, 'topSpeed', '100mph'); + +// Get an inherited property (1981) +console.log(driver.dateOfBirth); + +// Get the property we set (100mph) +console.log(driver.topSpeed); + +//*******************************************************// +// Basic Constructors +//*******************************************************// + +//********************** Snippet 1 **********************// + +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance +// [ES2015+] We used new template literals for string interpolation +class Car { + constructor(model, year, miles) { + this.model = model; + this.year = year; + this.miles = miles; + } + + toString() { + return `${this.model} has done ${this.miles} miles`; + } +} + +// Usage: + +// We can create new instances of the car +// [ES2015+] We used new keyword const for immutable constant declaration +const civic = new Car('Honda Civic', 2009, 20000); +const mondeo = new Car('Ford Mondeo', 2010, 5000); + +// and then open our browser console to view the +// output of the toString() method being called on +// these objects +console.log(civic.toString()); +console.log(mondeo.toString()); + +//*******************************************************// +// Constructors With Prototypes +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] All of it new syntax sugar above old function structures +class Car { + constructor(model, year, miles) { + this.model = model; + this.year = year; + this.miles = miles; + } +} + +// Note here that we are using Object.prototype.newMethod rather than +// Object.prototype so as to avoid redefining the prototype object +// [ES2015+] We still could use Object.prototype for adding new methods, because internally we use the same structure +// [ES2015+] We used new template literals for string interpolation +Car.prototype.toString = function() { + return `${this.model} has done ${this.miles} miles`; +}; + +// Usage: +// [ES2015+] We used new keyword const for immutable constant declaration +const civic = new Car('Honda Civic', 2009, 20000); +const mondeo = new Car('Ford Mondeo', 2010, 5000); + +console.log(civic.toString()); +console.log(mondeo.toString()); diff --git a/book/snippets/01-JavaScript-Design-Patterns/1-the-constructor-pattern.es5.js b/book/snippets/01-JavaScript-Design-Patterns/1-the-constructor-pattern.es5.js new file mode 100644 index 00000000..5794b25d --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/1-the-constructor-pattern.es5.js @@ -0,0 +1,174 @@ +//*******************************************************// +// Object Creation +//*******************************************************// + +//********************** Snippet 1 **********************// + +// Each of the following options will create a new empty object: + +var newObject = {}; + +// or +var newObject = Object.create( Object.prototype ); + +// or +var newObject = new Object(); + + +//********************** Snippet 2 **********************// + +// ECMAScript 3 compatible approaches + +// 1. Dot syntax + +// Set properties +newObject.someKey = "Hello World"; + +// Get properties +var value = newObject.someKey; + + + +// 2. Square bracket syntax + +// Set properties +newObject["someKey"] = "Hello World"; + +// Get properties +var value = newObject["someKey"]; + + + +// ECMAScript 5 only compatible approaches +// For more information see: http://kangax.github.com/es5-compat-table/ + +// 3. Object.defineProperty + +// Set properties +Object.defineProperty( newObject, "someKey", { + value: "for more control of the property's behavior", + writable: true, + enumerable: true, + configurable: true +}); + +// If the above feels a little difficult to read, a short-hand could +// be written as follows: + +var defineProp = function ( obj, key, value ){ + var config = { + value: value, + writable: true, + enumerable: true, + configurable: true + }; + Object.defineProperty( obj, key, config ); +}; + +// To use, we then create a new empty "person" object +var person = Object.create( Object.prototype ); + +// Populate the object with properties +defineProp( person, "car", "Delorean" ); +defineProp( person, "dateOfBirth", "1981" ); +defineProp( person, "hasBeard", false ); + +console.log(person); +// Outputs: Object {car: "Delorean", dateOfBirth: "1981", hasBeard: false} + + +// 4. Object.defineProperties + +// Set properties +Object.defineProperties( newObject, { + + "someKey": { + value: "Hello World", + writable: true + }, + + "anotherKey": { + value: "Foo bar", + writable: false + } + +}); + +// Getting properties for 3. and 4. can be done using any of the +// options in 1. and 2. + + +//********************** Snippet 3 **********************// + +// Usage: + +// Create a race car driver that inherits from the person object +var driver = Object.create( person ); + +// Set some properties for the driver +defineProp(driver, "topSpeed", "100mph"); + +// Get an inherited property (1981) +console.log( driver.dateOfBirth ); + +// Get the property we set (100mph) +console.log( driver.topSpeed ); + + +//*******************************************************// +// Basic Constructors +//*******************************************************// + +//********************** Snippet 1 **********************// + +function Car( model, year, miles ) { + + this.model = model; + this.year = year; + this.miles = miles; + + this.toString = function () { + return this.model + " has done " + this.miles + " miles"; + }; +} + +// Usage: + +// We can create new instances of the car +var civic = new Car( "Honda Civic", 2009, 20000 ); +var mondeo = new Car( "Ford Mondeo", 2010, 5000 ); + +// and then open our browser console to view the +// output of the toString() method being called on +// these objects +console.log( civic.toString() ); +console.log( mondeo.toString() ); + +//*******************************************************// +// Constructors With Prototypes +//*******************************************************// + +//********************** Snippet 1 **********************// + +function Car( model, year, miles ) { + + this.model = model; + this.year = year; + this.miles = miles; + +} + + +// Note here that we are using Object.prototype.newMethod rather than +// Object.prototype so as to avoid redefining the prototype object +Car.prototype.toString = function () { + return this.model + " has done " + this.miles + " miles"; +}; + +// Usage: + +var civic = new Car( "Honda Civic", 2009, 20000 ); +var mondeo = new Car( "Ford Mondeo", 2010, 5000 ); + +console.log( civic.toString() ); +console.log( mondeo.toString() ); \ No newline at end of file diff --git a/book/snippets/01-JavaScript-Design-Patterns/10-the-factory-pattern.es2015.js b/book/snippets/01-JavaScript-Design-Patterns/10-the-factory-pattern.es2015.js new file mode 100644 index 00000000..c4bc5c1b --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/10-the-factory-pattern.es2015.js @@ -0,0 +1,168 @@ +//********************** Snippet 1 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used the destructuring assignment syntax that makes it possible to unpack values from data structures into distinct variables. + +// Types.js - Constructors used behind the scenes + +// A constructor for defining new cars +class Car { + constructor({ doors, state, color }) { + // some defaults + this.doors = doors || 4; + this.state = state || 'brand new'; + this.color = color || 'silver'; + } +} +// A constructor for defining new trucks +class Truck { + constructor({ state, wheelSize, color }) { + this.state = state || 'used'; + this.wheelSize = wheelSize || 'large'; + this.color = color || 'blue'; + } +} + +// FactoryExample.js + +// Define a vehicle factory +class VehicleFactory { + // Define the prototypes and utilities for this factory + + // Our default vehicleClass is Car + constructor() { + this.vehicleClass = Car; + } + // Our Factory method for creating new Vehicle instances + createVehicle(options) { + switch (options.vehicleType) { + case 'car': + this.vehicleClass = Car; + break; + case 'truck': + this.vehicleClass = Truck; + break; + //defaults to VehicleFactory.prototype.vehicleClass (Car) + } + + return new this.vehicleClass(options); + } +} + +// Create an instance of our factory that makes cars +const carFactory = new VehicleFactory(); +const car = carFactory.createVehicle({ + vehicleType: 'car', + color: 'yellow', + doors: 6, +}); + +// Test to confirm our car was created using the vehicleClass/prototype Car + +// Outputs: true +console.log(car instanceof Car); + +// Outputs: Car object of color "yellow", doors: 6 in a "brand new" state +console.log(car); + +//********************** Snippet 2 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration + +const movingTruck = carFactory.createVehicle({ + vehicleType: 'truck', + state: 'like new', + color: 'red', + wheelSize: 'small', +}); + +// Test to confirm our truck was created with the vehicleClass/prototype Truck + +// Outputs: true +console.log(movingTruck instanceof Truck); + +// Outputs: Truck object of color "red", a "like new" state +// and a "small" wheelSize +console.log(movingTruck); + +//********************** Snippet 3 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] The extends keyword is used to create a class which is a child of another class. +// [ES2015+] We have new pattern implementation with new inheritance +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] A constructor can use the super keyword to call the constructor of the super class. +class TruckFactory extends VehicleFactory { + constructor() { + super(); + this.vehicleClass = Truck; + } +} +const truckFactory = new TruckFactory(); +const myBigTruck = truckFactory.createVehicle({ + state: 'omg..so bad.', + color: 'pink', + wheelSize: 'so big', +}); + +// Confirms that myBigTruck was created with the prototype Truck +// Outputs: true +console.log(myBigTruck instanceof Truck); + +// Outputs: Truck object with the color "pink", wheelSize "so big" +// and state "omg. so bad" +console.log(myBigTruck); + +//*******************************************************// +// Abstract Factories +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance +// [ES2015+] The static keyword defines a static method for a class. +// [ES2015+] Static methods are called without instantiating their class and cannot be called through a class instance. +// [ES2015+] Static methods are often used to create utility functions for an application. +// [ES2015+] We used new keyword const for immutable constant declaration + +class AbstractVehicleFactory { + constructor() { + // Storage for our vehicle types + this.types = {}; + } + + static getVehicle(type, customizations) { + const Vehicle = this.types[type]; + + return Vehicle ? new Vehicle(customizations) : null; + } + + static registerVehicle(type, Vehicle) { + const proto = Vehicle.prototype; + + // only register classes that fulfill the vehicle contract + if (proto.drive && proto.breakDown) { + this.types[type] = Vehicle; + } + + return abstractVehicleFactory; + } +} + +// Usage: + +abstractVehicleFactory.registerVehicle('car', Car); +abstractVehicleFactory.registerVehicle('truck', Truck); + +// Instantiate a new car based on the abstract vehicle type +const car = abstractVehicleFactory.getVehicle('car', { + color: 'lime green', + state: 'like new', +}); + +// Instantiate a new truck in a similar manner +const truck = abstractVehicleFactory.getVehicle('truck', { + wheelSize: 'medium', + color: 'neon yellow', +}); diff --git a/book/snippets/01-JavaScript-Design-Patterns/10-the-factory-pattern.es5.js b/book/snippets/01-JavaScript-Design-Patterns/10-the-factory-pattern.es5.js new file mode 100644 index 00000000..76349026 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/10-the-factory-pattern.es5.js @@ -0,0 +1,148 @@ +//********************** Snippet 1 **********************// + +// Types.js - Constructors used behind the scenes + +// A constructor for defining new cars +function Car( options ) { + + // some defaults + this.doors = options.doors || 4; + this.state = options.state || "brand new"; + this.color = options.color || "silver"; + + } + + // A constructor for defining new trucks + function Truck( options){ + + this.state = options.state || "used"; + this.wheelSize = options.wheelSize || "large"; + this.color = options.color || "blue"; + } + + + // FactoryExample.js + + // Define a skeleton vehicle factory + function VehicleFactory() {} + + // Define the prototypes and utilities for this factory + + // Our default vehicleClass is Car + VehicleFactory.prototype.vehicleClass = Car; + + // Our Factory method for creating new Vehicle instances + VehicleFactory.prototype.createVehicle = function ( options ) { + + switch(options.vehicleType){ + case "car": + this.vehicleClass = Car; + break; + case "truck": + this.vehicleClass = Truck; + break; + //defaults to VehicleFactory.prototype.vehicleClass (Car) + } + + return new this.vehicleClass( options ); + + }; + + // Create an instance of our factory that makes cars + var carFactory = new VehicleFactory(); + var car = carFactory.createVehicle( { + vehicleType: "car", + color: "yellow", + doors: 6 } ); + + // Test to confirm our car was created using the vehicleClass/prototype Car + + // Outputs: true + console.log( car instanceof Car ); + + // Outputs: Car object of color "yellow", doors: 6 in a "brand new" state + console.log( car ); + + //********************** Snippet 2 **********************// + + var movingTruck = carFactory.createVehicle( { + vehicleType: "truck", + state: "like new", + color: "red", + wheelSize: "small" } ); + +// Test to confirm our truck was created with the vehicleClass/prototype Truck + +// Outputs: true +console.log( movingTruck instanceof Truck ); + +// Outputs: Truck object of color "red", a "like new" state +// and a "small" wheelSize +console.log( movingTruck ); + +//********************** Snippet 3 **********************// + +function TruckFactory () {} +TruckFactory.prototype = new VehicleFactory(); +TruckFactory.prototype.vehicleClass = Truck; + +var truckFactory = new TruckFactory(); +var myBigTruck = truckFactory.createVehicle( { + state: "omg..so bad.", + color: "pink", + wheelSize: "so big" } ); + +// Confirms that myBigTruck was created with the prototype Truck +// Outputs: true +console.log( myBigTruck instanceof Truck ); + +// Outputs: Truck object with the color "pink", wheelSize "so big" +// and state "omg. so bad" +console.log( myBigTruck ); + +//*******************************************************// +// Abstract Factories +//*******************************************************// + +//********************** Snippet 1 **********************// + +var abstractVehicleFactory = (function () { + + // Storage for our vehicle types + var types = {}; + + return { + getVehicle: function ( type, customizations ) { + var Vehicle = types[type]; + + return (Vehicle ? new Vehicle(customizations) : null); + }, + + registerVehicle: function ( type, Vehicle ) { + var proto = Vehicle.prototype; + + // only register classes that fulfill the vehicle contract + if ( proto.drive && proto.breakDown ) { + types[type] = Vehicle; + } + + return abstractVehicleFactory; + } + }; + })(); + + + // Usage: + + abstractVehicleFactory.registerVehicle( "car", Car ); + abstractVehicleFactory.registerVehicle( "truck", Truck ); + + // Instantiate a new car based on the abstract vehicle type + var car = abstractVehicleFactory.getVehicle( "car", { + color: "lime green", + state: "like new" } ); + + // Instantiate a new truck in a similar manner + var truck = abstractVehicleFactory.getVehicle( "truck", { + wheelSize: "medium", + color: "neon yellow" } ); \ No newline at end of file diff --git a/book/snippets/01-JavaScript-Design-Patterns/11-the-mixin-pattern.es2015-2.js b/book/snippets/01-JavaScript-Design-Patterns/11-the-mixin-pattern.es2015-2.js new file mode 100644 index 00000000..31bf8ae7 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/11-the-mixin-pattern.es2015-2.js @@ -0,0 +1,115 @@ +//*******************************************************// +// Mixins +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new method declaration +// [ES2015+] We used new arrow function syntax +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] Class can be used as an expression as well as a statement. As an expression it returns a new class each time it's evaluated. +// [ES2015+] The extends keyword is used to create a class which is a child of another class. +// [ES2015+] The extends clause accepts arbitrary expressions that return classes or constructors +// [ES2015+] All we need to define a mixin is a function that accepts a superclass and creates a new subclass from it, like this: + +const MyMixins = superclass => + class extends superclass { + moveUp() { + console.log('move up'); + } + moveDown() { + console.log('move down'); + } + stop() { + console.log('stop! in the name of love!'); + } + }; + +//********************** Snippet 2 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] We used new arrow function syntax +// [ES2015+] We have new pattern implementation with new inheritance + +// A skeleton carAnimator constructor +class CarAnimator { + moveLeft() { + console.log('move left'); + } +} +// A skeleton personAnimator constructor +class PersonAnimator { + moveRandomly() { + /*..*/ + } +} + +// [ES2015+] Then we can use it in an extends clause like this: +class MyAnimator extends MyMixins(CarAnimator) {} + +// Create a new instance of carAnimator +const myAnimator = new MyAnimator(); +myAnimator.moveLeft(); +myAnimator.moveDown(); +myAnimator.stop(); + +// Outputs: +// move left +// move down +// stop! in the name of love! + +//********************** Snippet 3 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance +// [ES2015+] The extends keyword is used to create a class which is a child of another class. +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax + +// Define a simple Car constructor +class Car { + constructor({ model, color }) { + this.model = model || 'no model provided'; + this.color = color || 'no colour provided'; + } +} + +// Mixin +const Mixin = superclass => + class extends superclass { + driveForward() { + console.log('drive forward'); + } + driveBackward() { + console.log('drive backward'); + } + driveSideways() { + console.log('drive sideways'); + } + }; + +class MyCar extends Mixin(Car) {} + +// Create a new Car +const myCar = new MyCar({ + model: 'Ford Escort', + color: 'blue', +}); + +// Test to make sure we now have access to the methods +myCar.driveForward(); +myCar.driveBackward(); + +// Outputs: +// drive forward +// drive backward + +const mySportCar = new MyCar({ + model: 'Porsche', + color: 'red', +}); + +mySportsCar.driveSideways(); + +// Outputs: +// drive sideways diff --git a/book/snippets/01-JavaScript-Design-Patterns/11-the-mixin-pattern.es2015.js b/book/snippets/01-JavaScript-Design-Patterns/11-the-mixin-pattern.es2015.js new file mode 100644 index 00000000..94b1cd2d --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/11-the-mixin-pattern.es2015.js @@ -0,0 +1,186 @@ +//*******************************************************// +// Sub-classing +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method + +class Person { + constructor(firstName, lastName) { + this.firstName = firstName; + this.lastName = lastName; + this.gender = 'male'; + } +} + +//********************** Snippet 2 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method +// [ES2015+] We have new pattern implementation with new inheritance + +// a new instance of Person can then easily be created as follows: +const clark = new Person('Clark', 'Kent'); + +// Define a subclass constructor for for "Superhero": +// [ES2015+] A constructor can use the super keyword to call the constructor of the super class. +class Superhero extends Person { + constructor(firstName, lastName, powers) { + // Invoke the superclass constructor on the new object + // then use .call() to invoke the constructor as a method of + // the object to be initialized. + + super(firstName, lastName); + + // Finally, store their powers, a new array of traits not found in a normal "Person" + this.powers = powers; + } +} + +const superman = new Superhero('Clark', 'Kent', ['flight', 'heat-vision']); +console.log(superman); + +// Outputs Person attributes as well as powers + +//*******************************************************// +// Mixins +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new method declaration + +const myMixins = { + moveUp() { + console.log('move up'); + }, + + moveDown() { + console.log('move down'); + }, + + stop() { + console.log('stop! in the name of love!'); + }, +}; + +//********************** Snippet 2 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] We used new arrow function syntax +// [ES2015+] We have new pattern implementation with new inheritance + +// A skeleton carAnimator constructor +class CarAnimator { + moveLeft() { + console.log('move left'); + } +} +// A skeleton personAnimator constructor +class PersonAnimator { + moveRandomly() { + /*..*/ + } +} +// [ES2015+] New Object.assign() method copies enumerable and own properties from a source object (second argument) to a target object (first argument). +Object.assign(CarAnimator.prototype, myMixins); +Object.assign(PersonAnimator.prototype, myMixins); + +// Create a new instance of carAnimator +const myAnimator = new CarAnimator(); +myAnimator.moveLeft(); +myAnimator.moveDown(); +myAnimator.stop(); + +// Outputs: +// move left +// move down +// stop! in the name of love! + +//********************** Snippet 3 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance +// [ES2015+] We still could use Object.prototype for adding new methods, because internally we use the same structure +// [ES2015+] We used the destructuring assignment syntax that makes it possible to unpack values from data structures into distinct variables. +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax + +// Define a simple Car constructor +class Car { + constructor({ model, color }) { + this.model = model || 'no model provided'; + this.color = color || 'no colour provided'; + } +} + +// Mixin +class Mixin { + driveForward() { + console.log('drive forward'); + } + + driveBackward() { + console.log('drive backward'); + } + + driveSideways() { + console.log('drive sideways'); + } +} + +// Extend an existing object with a method from another +// [ES2015+] The rest parameter syntax allows us to represent an indefinite number of arguments as an array. +const augment = (receivingClass, givingClass, ...methodsNames) => { + // only provide certain methods + if (methodsNames.length !== 0) { + // [ES2015+] New function map calls a provided callback function once for each element in an array, in order. + methodsNames.map(methodName => { + receivingClass.prototype[methodName] = givingClass.prototype[methodName]; + }); + + // provide all methods + } else { + // [ES2015+] New method Object.getOwnPropertyNames() returns an array of all properties (including non-enumerable properties) + Object.getOwnPropertyNames(givingClass.prototype).map(methodName => { + // check to make sure the receiving class doesn't + // have a method of the same name as the one currently + // being processed + if (!Object.hasOwnProperty.call(receivingClass.prototype, methodName)) { + receivingClass.prototype[methodName] = givingClass.prototype[methodName]; + } + }); + } +}; + +// Augment the Car constructor to include "driveForward" and "driveBackward" +augment(Car, Mixin, 'driveForward', 'driveBackward'); + +// Create a new Car +const myCar = new Car({ + model: 'Ford Escort', + color: 'blue', +}); + +// Test to make sure we now have access to the methods +myCar.driveForward(); +myCar.driveBackward(); + +// Outputs: +// drive forward +// drive backward + +// We can also augment Car to include all functions from our mixin +// by not explicitly listing a selection of them +augment(Car, Mixin); + +const mySportsCar = new Car({ + model: 'Porsche', + color: 'red', +}); + +mySportsCar.driveSideways(); + +// Outputs: +// drive sideways diff --git a/book/snippets/01-JavaScript-Design-Patterns/11-the-mixin-pattern.es5.js b/book/snippets/01-JavaScript-Design-Patterns/11-the-mixin-pattern.es5.js new file mode 100644 index 00000000..baa66274 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/11-the-mixin-pattern.es5.js @@ -0,0 +1,178 @@ +//*******************************************************// +// Sub-classing +//*******************************************************// + +//********************** Snippet 1 **********************// + +var Person = function( firstName, lastName ){ + + this.firstName = firstName; + this.lastName = lastName; + this.gender = "male"; + + }; + +//********************** Snippet 2 **********************// + +// a new instance of Person can then easily be created as follows: +var clark = new Person( "Clark", "Kent" ); + +// Define a subclass constructor for for "Superhero": +var Superhero = function( firstName, lastName, powers ){ + + // Invoke the superclass constructor on the new object + // then use .call() to invoke the constructor as a method of + // the object to be initialized. + + Person.call( this, firstName, lastName ); + + // Finally, store their powers, a new array of traits not found in a normal "Person" + this.powers = powers; +}; + +Superhero.prototype = Object.create( Person.prototype ); +var superman = new Superhero( "Clark", "Kent", ["flight","heat-vision"] ); +console.log( superman ); + +// Outputs Person attributes as well as powers + +//*******************************************************// +// Mixins +//*******************************************************// + +//********************** Snippet 1 **********************// + +var myMixins = { + + moveUp: function(){ + console.log( "move up" ); + }, + + moveDown: function(){ + console.log( "move down" ); + }, + + stop: function(){ + console.log( "stop! in the name of love!" ); + } + + }; + +//********************** Snippet 2 **********************// + +// A skeleton carAnimator constructor +function CarAnimator(){ + this.moveLeft = function(){ + console.log( "move left" ); + }; + } + + // A skeleton personAnimator constructor + function PersonAnimator(){ + this.moveRandomly = function(){ /*..*/ }; + } + + // Extend both constructors with our Mixin + _.extend( CarAnimator.prototype, myMixins ); + _.extend( PersonAnimator.prototype, myMixins ); + + // Create a new instance of carAnimator + var myAnimator = new CarAnimator(); + myAnimator.moveLeft(); + myAnimator.moveDown(); + myAnimator.stop(); + + // Outputs: + // move left + // move down + // stop! in the name of love! + +//********************** Snippet 3 **********************// + + // Define a simple Car constructor +var Car = function ( settings ) { + + this.model = settings.model || "no model provided"; + this.color = settings.color || "no colour provided"; + +}; + +// Mixin +var Mixin = function () {}; + +Mixin.prototype = { + + driveForward: function () { + console.log( "drive forward" ); + }, + + driveBackward: function () { + console.log( "drive backward" ); + }, + + driveSideways: function () { + console.log( "drive sideways" ); + } + +}; + + +// Extend an existing object with a method from another +function augment( receivingClass, givingClass ) { + + // only provide certain methods + if ( arguments[2] ) { + for ( var i = 2, len = arguments.length; i < len; i++ ) { + receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]]; + } + } + // provide all methods + else { + for ( var methodName in givingClass.prototype ) { + + // check to make sure the receiving class doesn't + // have a method of the same name as the one currently + // being processed + if ( !Object.hasOwnProperty.call(receivingClass.prototype, methodName) ) { + receivingClass.prototype[methodName] = givingClass.prototype[methodName]; + } + + // Alternatively (check prototype chain as well): + // if ( !receivingClass.prototype[methodName] ) { + // receivingClass.prototype[methodName] = givingClass.prototype[methodName]; + // } + } + } +} + + +// Augment the Car constructor to include "driveForward" and "driveBackward" +augment( Car, Mixin, "driveForward", "driveBackward" ); + +// Create a new Car +var myCar = new Car({ + model: "Ford Escort", + color: "blue" +}); + +// Test to make sure we now have access to the methods +myCar.driveForward(); +myCar.driveBackward(); + +// Outputs: +// drive forward +// drive backward + +// We can also augment Car to include all functions from our mixin +// by not explicitly listing a selection of them +augment( Car, Mixin ); + +var mySportsCar = new Car({ + model: "Porsche", + color: "red" +}); + +mySportsCar.driveSideways(); + +// Outputs: +// drive sideways \ No newline at end of file diff --git a/book/snippets/01-JavaScript-Design-Patterns/12-the-decoration-pattern.es2015.js b/book/snippets/01-JavaScript-Design-Patterns/12-the-decoration-pattern.es2015.js new file mode 100644 index 00000000..dbfe976a --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/12-the-decoration-pattern.es2015.js @@ -0,0 +1,379 @@ +//********************** Snippet 1 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method +// [ES2015+] We used new keyword const for immutable constant declaration + +// A vehicle constructor +class Vehicle { + constructor(vehicleType) { + // some sane defaults + this.vehicleType = vehicleType || 'car'; + this.model = 'default'; + this.license = '00000-000'; + } +} + +// Test instance for a basic vehicle +const testInstance = new Vehicle('car'); +console.log(testInstance); + +// Outputs: +// vehicle: car, model:default, license: 00000-000 + +// Lets create a new instance of vehicle, to be decorated +const truck = new Vehicle('truck'); + +// New functionality we're decorating vehicle with +truck.setModel = function(modelName) { + this.model = modelName; +}; + +truck.setColor = function(color) { + this.color = color; +}; + +// Test the value setters and value assignment works correctly +truck.setModel('CAT'); +truck.setColor('blue'); + +console.log(truck); + +// Outputs: +// vehicle:truck, model:CAT, color: blue + +// Demonstrate "vehicle" is still unaltered +const secondInstance = new Vehicle('car'); +console.log(secondInstance); + +// Outputs: +// vehicle: car, model:default, license: 00000-000 + +//********************** Snippet 2 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance +// [ES2015+] We have new pattern implementation with new inheritance +// [ES2015+] We used new constructor method +// [ES2015+] We used new keyword const for immutable constant declaration +// [SE2015+] We used new keyword let, which declares a block scope local variable + +// The constructor to decorate +class MacBook { + constructor() { + this.cost = 997; + this.screenSize = 11.6; + } + getCost() { + return this.cost; + } + getScreenSize() { + return this.screenSize; + } +} + +// Decorator 1 +// [ES2015+] The extends keyword is used to create a class which is a child of another class. +// [ES2015+] A constructor can use the super keyword to call the constructor of the super class. +class Memory extends MacBook { + constructor(macBook) { + super(); + this.macBook = macBook; + } + + getCost() { + return this.macBook.getCost() + 75; + } +} + +// Decorator 2 +class Engraving extends MacBook { + constructor(macBook) { + super(); + this.macBook = macBook; + } + + getCost() { + return this.macBook.getCost() + 200; + } +} + +// Decorator 3 +class Insurance extends MacBook { + constructor(macBook) { + super(); + this.macBook = macBook; + } + + getCost() { + return this.macBook.getCost() + 250; + } +} + +// init main object +let mb = new MacBook(); + +// init decorators +mb = new Memory(mb); +mb = new Engraving(mb); +mb = new Insurance(mb); + +// Outputs: 1522 +console.log(mb.getCost()); + +// Outputs: 11.6 +console.log(mb.getScreenSize()); + +//*******************************************************// +// Pseudo-classical Decorators +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used the destructuring assignment syntax that makes it possible to unpack values from data structures into distinct variables. + +// Create interfaces using a pre-defined Interface +// constructor that accepts an interface name and +// skeleton methods to expose. + +// In our reminder example summary() and placeOrder() +// represent functionality the interface should +// support +const reminder = new Interface('List', ['summary', 'placeOrder']); + +const properties = { + name: 'Remember to buy the milk', + date: '05/06/2016', + actions: { + summary() { + return 'Remember to buy the milk, we are almost out!'; + }, + placeOrder() { + return 'Ordering milk from your local grocery store'; + }, + }, +}; + +// Now create a constructor implementing the above properties +// and methods + +class Todo { + constructor({ actions, name }) { + // State the methods we expect to be supported + // as well as the Interface instance being checked + // against + + Interface.ensureImplements(actions, reminder); + + this.name = name; + this.methods = actions; + } +} + +// Create a new instance of our Todo constructor + +const todoItem = new Todo(properties); + +// Finally test to make sure these function correctly + +console.log(todoItem.methods.summary()); +console.log(todoItem.methods.placeOrder()); + +// Outputs: +// Remember to buy the milk, we are almost out! +// Ordering milk from your local grocery store + +//********************** Snippet 2 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] Below we used new class expression, using keyword class + +const Macbook = class { + //... +}; + +const MacbookWith4GBRam = class {}; +const MacbookWith8GBRam = class {}; +const MacbookWith4GBRamAndEngraving = class {}; +const MacbookWith8GBRamAndEngraving = class {}; +const MacbookWith8GBRamAndParallels = class {}; +const MacbookWith4GBRamAndParallels = class {}; +const MacbookWith8GBRamAndParallelsAndCase = class {}; +const MacbookWith4GBRamAndParallelsAndCase = class {}; +const MacbookWith8GBRamAndParallelsAndCaseAndInsurance = class {}; +const MacbookWith4GBRamAndParallelsAndCaseAndInsurance = class {}; + +//********************** Snippet 3 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] Below we used new class declaration, using keyword class + +const Macbook = new Interface('Macbook', [ + 'addEngraving', + 'addParallels', + 'add4GBRam', + 'add8GBRam', + 'addCase', +]); + +// A Macbook Pro might thus be represented as follows: +class MacbookPro { + // implements Macbook +} + +// [ES2015+] We still could use Object.prototype for adding new methods, because internally we use the same structure +MacbookPro.prototype = { + addEngraving() {}, + addParallels() {}, + add4GBRam() {}, + add8GBRam() {}, + addCase() {}, + getPrice() { + // Base price + return 900.0; + }, +}; + +//********************** Snippet 4 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration + +// Macbook decorator abstract decorator class + +class MacbookDecorator { + constructor(macbook) { + Interface.ensureImplements(macbook, Macbook); + this.macbook = macbook; + } + + addEngraving() { + return this.macbook.addEngraving(); + } + + addParallels() { + return this.macbook.addParallels(); + } + + add4GBRam() { + return this.macbook.add4GBRam(); + } + + add8GBRam() { + return this.macbook.add8GBRam(); + } + + addCase() { + return this.macbook.addCase(); + } + + getPrice() { + return this.macbook.getPrice(); + } +} + +//********************** Snippet 5 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] We have new pattern implementation with new inheritance +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax +// [ES2015+] We used new template literals for string interpolation + +// Let's now extend (decorate) the CaseDecorator +// with a MacbookDecorator +// [ES2015+] The extends keyword is used to create a class which is a child of another class. +// [ES2015+] A constructor can use the super keyword to call the constructor of the super class. +class CaseDecorator extends MacbookDecorator { + constructor(macbook) { + super(macbook); + } + + addCase() { + return `${this.macbook.addCase()}Adding case to macbook`; + } + + getPrice() { + return this.macbook.getPrice() + 45.0; + } +} + +//********************** Snippet 6 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration + +// Instantiation of the macbook +const myMacbookPro = new MacbookPro(); + +// Outputs: 900.00 +console.log(myMacbookPro.getPrice()); + +// Decorate the macbook +const decoratedMacbookPro = new CaseDecorator(myMacbookPro); + +// This will return 945.00 +console.log(decoratedMacbookPro.getPrice()); + +//*******************************************************// +// Decorators With jQuery +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new keyword let for mutable variable declaration +// [ES2015+] We used new arrow function syntax +// [ES2015+] We used new template literals for string interpolation + +let decoratorApp = decoratorApp || {}; + +// define the objects we're going to use +decoratorApp = { + defaults: { + validate: false, + limit: 5, + name: 'foo', + welcome() { + console.log('welcome!'); + }, + }, + + options: { + validate: true, + name: 'bar', + helloWorld() { + console.log('hello world'); + }, + }, + + settings: {}, + + printObj(obj) { + const arr = []; + let next; + $.each(obj, (key, val) => { + next = `${key}: `; + next += $.isPlainObject(val) ? printObj(val) : val; + arr.push(next); + }); + + return `{ ${arr.join(', ')} }`; + }, +}; + +// merge defaults and options, without modifying defaults explicitly +decoratorApp.settings = $.extend( + {}, + decoratorApp.defaults, + decoratorApp.options +); + +// what we have done here is decorated defaults in a way that provides +// access to the properties and functionality it has to offer (as well as +// that of the decorator "options"). defaults itself is left unchanged + +$('#log').append( + decoratorApp.printObj(decoratorApp.settings) + + +decoratorApp.printObj(decoratorApp.options) + + +decoratorApp.printObj(decoratorApp.defaults) +); + +// settings -- { validate: true, limit: 5, name: bar, welcome: function (){ console.log( "welcome!" ); }, +// helloWorld: function (){ console.log( "hello world" ); } } +// options -- { validate: true, name: bar, helloWorld: function (){ console.log( "hello world" ); } } +// defaults -- { validate: false, limit: 5, name: foo, welcome: function (){ console.log("welcome!"); } } diff --git a/book/snippets/01-JavaScript-Design-Patterns/12-the-decoration-pattern.es5.js b/book/snippets/01-JavaScript-Design-Patterns/12-the-decoration-pattern.es5.js new file mode 100644 index 00000000..2a3aa1e4 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/12-the-decoration-pattern.es5.js @@ -0,0 +1,339 @@ +//********************** Snippet 1 **********************// + +// A vehicle constructor +function Vehicle( vehicleType ){ + + // some sane defaults + this.vehicleType = vehicleType || "car"; + this.model = "default"; + this.license = "00000-000"; + +} + +// Test instance for a basic vehicle +var testInstance = new Vehicle( "car" ); +console.log( testInstance ); + +// Outputs: +// vehicle: car, model:default, license: 00000-000 + +// Lets create a new instance of vehicle, to be decorated +var truck = new Vehicle( "truck" ); + +// New functionality we're decorating vehicle with +truck.setModel = function( modelName ){ + this.model = modelName; +}; + +truck.setColor = function( color ){ + this.color = color; +}; + +// Test the value setters and value assignment works correctly +truck.setModel( "CAT" ); +truck.setColor( "blue" ); + +console.log( truck ); + +// Outputs: +// vehicle:truck, model:CAT, color: blue + +// Demonstrate "vehicle" is still unaltered +var secondInstance = new Vehicle( "car" ); +console.log( secondInstance ); + +// Outputs: +// vehicle: car, model:default, license: 00000-000 + +//********************** Snippet 2 **********************// + +// The constructor to decorate +function MacBook() { + + this.cost = function () { return 997; }; + this.screenSize = function () { return 11.6; }; + + } + + // Decorator 1 + function memory( macbook ) { + + var v = macbook.cost(); + macbook.cost = function() { + return v + 75; + }; + + } + + // Decorator 2 + function engraving( macbook ){ + + var v = macbook.cost(); + macbook.cost = function(){ + return v + 200; + }; + + } + + // Decorator 3 + function insurance( macbook ){ + + var v = macbook.cost(); + macbook.cost = function(){ + return v + 250; + }; + + } + + var mb = new MacBook(); + memory( mb ); + engraving( mb ); + insurance( mb ); + + // Outputs: 1522 + console.log( mb.cost() ); + + // Outputs: 11.6 + console.log( mb.screenSize() ); + + +//*******************************************************// +// Pseudo-classical Decorators +//*******************************************************// + +//********************** Snippet 1 **********************// + +// Create interfaces using a pre-defined Interface +// constructor that accepts an interface name and +// skeleton methods to expose. + +// In our reminder example summary() and placeOrder() +// represent functionality the interface should +// support +var reminder = new Interface( "List", ["summary", "placeOrder"] ); + +var properties = { + name: "Remember to buy the milk", + date: "05/06/2016", + actions:{ + summary: function (){ + return "Remember to buy the milk, we are almost out!"; + }, + placeOrder: function (){ + return "Ordering milk from your local grocery store"; + } + } +}; + +// Now create a constructor implementing the above properties +// and methods + +function Todo( config ){ + + // State the methods we expect to be supported + // as well as the Interface instance being checked + // against + + Interface.ensureImplements( config.actions, reminder ); + + this.name = config.name; + this.methods = config.actions; + +} + +// Create a new instance of our Todo constructor + +var todoItem = new Todo( properties ); + +// Finally test to make sure these function correctly + +console.log( todoItem.methods.summary() ); +console.log( todoItem.methods.placeOrder() ); + +// Outputs: +// Remember to buy the milk, we are almost out! +// Ordering milk from your local grocery store + +//********************** Snippet 2 **********************// + +var Macbook = function(){ + //... +}; + +var MacbookWith4GBRam = function(){}, + MacbookWith8GBRam = function(){}, + MacbookWith4GBRamAndEngraving = function(){}, + MacbookWith8GBRamAndEngraving = function(){}, + MacbookWith8GBRamAndParallels = function(){}, + MacbookWith4GBRamAndParallels = function(){}, + MacbookWith8GBRamAndParallelsAndCase = function(){}, + MacbookWith4GBRamAndParallelsAndCase = function(){}, + MacbookWith8GBRamAndParallelsAndCaseAndInsurance = function(){}, + MacbookWith4GBRamAndParallelsAndCaseAndInsurance = function(){}; + + +//********************** Snippet 3 **********************// + +var Macbook = new Interface( "Macbook", + ["addEngraving", + "addParallels", + "add4GBRam", + "add8GBRam", + "addCase"]); + +// A Macbook Pro might thus be represented as follows: +var MacbookPro = function(){ + // implements Macbook +}; + +MacbookPro.prototype = { + addEngraving: function(){ + }, + addParallels: function(){ + }, + add4GBRam: function(){ + }, + add8GBRam:function(){ + }, + addCase: function(){ + }, + getPrice: function(){ + // Base price + return 900.00; + } +}; + +//********************** Snippet 4 **********************// +// Macbook decorator abstract decorator class + +var MacbookDecorator = function( macbook ){ + + Interface.ensureImplements( macbook, Macbook ); + this.macbook = macbook; + +}; + +MacbookDecorator.prototype = { + addEngraving: function(){ + return this.macbook.addEngraving(); + }, + addParallels: function(){ + return this.macbook.addParallels(); + }, + add4GBRam: function(){ + return this.macbook.add4GBRam(); + }, + add8GBRam:function(){ + return this.macbook.add8GBRam(); + }, + addCase: function(){ + return this.macbook.addCase(); + }, + getPrice: function(){ + return this.macbook.getPrice(); + } +}; + +//********************** Snippet 5 **********************// + +// First, define a way to extend an object a +// with the properties in object b. We'll use +// this shortly! +function extend( a, b ){ + for( var key in b ) + if( b.hasOwnProperty(key) ) + a[key] = b[key]; + return a; +} + +var CaseDecorator = function( macbook ){ + this.macbook = macbook; +}; + +// Let's now extend (decorate) the CaseDecorator +// with a MacbookDecorator +extend( CaseDecorator, MacbookDecorator ); + +CaseDecorator.prototype.addCase = function(){ + return this.macbook.addCase() + "Adding case to macbook"; +}; + +CaseDecorator.prototype.getPrice = function(){ + return this.macbook.getPrice() + 45.00; +}; + +//********************** Snippet 6 **********************// + +// Instantiation of the macbook +var myMacbookPro = new MacbookPro(); + +// Outputs: 900.00 +console.log( myMacbookPro.getPrice() ); + +// Decorate the macbook +var decoratedMacbookPro = new CaseDecorator( myMacbookPro ); + +// This will return 945.00 +console.log( decoratedMacbookPro.getPrice() ); + + +//*******************************************************// +// Decorators With jQuery +//*******************************************************// + +//********************** Snippet 1 **********************// + +var decoratorApp = decoratorApp || {}; + +// define the objects we're going to use +decoratorApp = { + + defaults: { + validate: false, + limit: 5, + name: "foo", + welcome: function () { + console.log( "welcome!" ); + } + }, + + options: { + validate: true, + name: "bar", + helloWorld: function () { + console.log( "hello world" ); + } + }, + + settings: {}, + + printObj: function ( obj ) { + var arr = [], + next; + $.each( obj, function ( key, val ) { + next = key + ": "; + next += $.isPlainObject(val) ? printObj( val ) : val; + arr.push( next ); + } ); + + return "{ " + arr.join(", ") + " }"; + } + +}; + +// merge defaults and options, without modifying defaults explicitly +decoratorApp.settings = $.extend({}, decoratorApp.defaults, decoratorApp.options); + +// what we have done here is decorated defaults in a way that provides +// access to the properties and functionality it has to offer (as well as +// that of the decorator "options"). defaults itself is left unchanged + +$("#log") + .append( decoratorApp.printObj(decoratorApp.settings) + + + decoratorApp.printObj(decoratorApp.options) + + + decoratorApp.printObj(decoratorApp.defaults)); + +// settings -- { validate: true, limit: 5, name: bar, welcome: function (){ console.log( "welcome!" ); }, +// helloWorld: function (){ console.log( "hello world" ); } } +// options -- { validate: true, name: bar, helloWorld: function (){ console.log( "hello world" ); } } +// defaults -- { validate: false, limit: 5, name: foo, welcome: function (){ console.log("welcome!"); } } diff --git a/book/snippets/01-JavaScript-Design-Patterns/13-flyweight-pattern.es2015.js b/book/snippets/01-JavaScript-Design-Patterns/13-flyweight-pattern.es2015.js new file mode 100644 index 00000000..08730cb6 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/13-flyweight-pattern.es2015.js @@ -0,0 +1,382 @@ +//*******************************************************// +// Implementing Classical Flyweights +//*******************************************************// +//********************** Snippet 1 **********************// +// Simulate pure virtual inheritance/"implement" keyword for JS +Function.prototype.implementsFor = function(parentClassOrObject) { + if (parentClassOrObject.constructor === Function) { + // Normal Inheritance + this.prototype = new parentClassOrObject(); + this.prototype.constructor = this; + this.prototype.parent = parentClassOrObject.prototype; + } else { + // Pure Virtual Inheritance + this.prototype = parentClassOrObject; + this.prototype.constructor = this; + this.prototype.parent = parentClassOrObject; + } + return this; +}; + + +//********************** Snippet 2 **********************// +// [ES2015+] We used new template literals for string interpolation +// [ES2015+] We used new keyword const for immutable constant declaration +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new arrow function syntax +// [ES2015+] Parentheses are optional when there is only one parameter + +// Flyweight object +const CoffeeOrder = { + // Interfaces + serveCoffee(context) {}, + getFlavor() {}, +}; + +// ConcreteFlyweight object that creates ConcreteFlyweight +// Implements CoffeeOrder +function CoffeeFlavor(newFlavor) { + const flavor = newFlavor; + + // If an interface has been defined for a feature + // implement the feature + if (typeof this.getFlavor === 'function') { + this.getFlavor = () => flavor; + } + + if (typeof this.serveCoffee === 'function') { + this.serveCoffee = context => { + console.log( + `Serving Coffee flavor ${flavor} to table number ${context.getTable()}` + ); + }; + } +} + +// Implement interface for CoffeeOrder +CoffeeFlavor.implementsFor(CoffeeOrder); + +// Handle table numbers for a coffee order +const CoffeeOrderContext = tableNumber => { + return { + getTable() { + return tableNumber; + }, + }; +}; + +const CoffeeFlavorFactory = () => { + const flavors = {}; + let length = 0; + + return { + getCoffeeFlavor(flavorName) { + let flavor = flavors[flavorName]; + if (typeof flavor === 'undefined') { + flavor = new CoffeeFlavor(flavorName); + flavors[flavorName] = flavor; + length++; + } + return flavor; + }, + + getTotalCoffeeFlavorsMade() { + return length; + }, + }; +}; + +// Sample usage: +// testFlyweight() + +const testFlyweight = () => { + // The flavors ordered. + const flavors = []; + + // The tables for the orders. + const tables = []; + + // Number of orders made + let ordersMade = 0; + + // The CoffeeFlavorFactory instance + const flavorFactory = new CoffeeFlavorFactory(); + + function takeOrders(flavorIn, table) { + flavors.push(flavorFactory.getCoffeeFlavor(flavorIn)); + tables.push(new CoffeeOrderContext(table)); + ordersMade++; + } + + takeOrders('Cappuccino', 2); + takeOrders('Cappuccino', 2); + takeOrders('Frappe', 1); + takeOrders('Frappe', 1); + takeOrders('Xpresso', 1); + takeOrders('Frappe', 897); + takeOrders('Cappuccino', 97); + takeOrders('Cappuccino', 97); + takeOrders('Frappe', 3); + takeOrders('Xpresso', 3); + takeOrders('Cappuccino', 3); + takeOrders('Xpresso', 96); + takeOrders('Frappe', 552); + takeOrders('Cappuccino', 121); + takeOrders('Xpresso', 121); + + for (let i = 0; i < ordersMade; ++i) { + flavors[i].serveCoffee(tables[i]); + } + console.log(' '); + console.log( + `total CoffeeFlavor objects made: ${flavorFactory.getTotalCoffeeFlavorsMade()}` + ); +}; + +//*******************************************************// +// Converting code to use the Flyweight pattern +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] We used new keyword const for immutable constant declaration + +class Book { + constructor( + id, + title, + author, + genre, + pageCount, + publisherID, + ISBN, + checkoutDate, + checkoutMember, + dueReturnDate, + availability + ) { + this.id = id; + this.title = title; + this.author = author; + this.genre = genre; + this.pageCount = pageCount; + this.publisherID = publisherID; + this.ISBN = ISBN; + this.checkoutDate = checkoutDate; + this.checkoutMember = checkoutMember; + this.dueReturnDate = dueReturnDate; + this.availability = availability; + } + + getTitle() { + return this.title; + } + + getAuthor() { + return this.author; + } + + getISBN() { + return this.ISBN; + } + + // For brevity, other getters are not shown + updateCheckoutStatus( + bookID, + newStatus, + checkoutDate, + checkoutMember, + newReturnDate + ) { + this.id = bookID; + this.availability = newStatus; + this.checkoutDate = checkoutDate; + this.checkoutMember = checkoutMember; + this.dueReturnDate = newReturnDate; + } + + extendCheckoutPeriod(bookID, newReturnDate) { + this.id = bookID; + this.dueReturnDate = newReturnDate; + } + + isPastDue(bookID) { + const currentDate = new Date(); + return currentDate.getTime() > Date.parse(this.dueReturnDate); + } +} + +//********************** Snippet 2 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration + +// Flyweight optimized version +const Book = function(title, author, genre, pageCount, publisherID, ISBN) { + this.title = title; + this.author = author; + this.genre = genre; + this.pageCount = pageCount; + this.publisherID = publisherID; + this.ISBN = ISBN; +}; + +//*******************************************************// +// Converting code to use the Flyweight pattern +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new arrow function syntax + +// Book Factory singleton +const existingBooks = {}; + +class BookFactory { + constructor(title, author, genre, pageCount, publisherID, ISBN) { + + // Find out if a particular book meta-data combination has been created before + // !! or (bang bang) forces a boolean to be returned + this.existingBook = existingBooks[ISBN]; + if (!!this.existingBook) { + return this.existingBook; + } else { + + // if not, let's create a new instance of the book and store it + const book = new Book(title, author, genre, pageCount, publisherID, ISBN); + existingBooks[ISBN] = book; + return book; + + } + } +} + + +//*******************************************************// +// Managing the extrinsic states +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] We used new keyword const for immutable constant declaration + +// BookRecordManager singleton +const bookRecordDatabase = {}; + +class BookRecordManager { + // add a new book into the library system + constructor(id, title, author, genre, pageCount, publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate, availability) { + this.book = new BookFactory(title, author, genre, pageCount, publisherID, ISBN); + + bookRecordDatabase[id] = { + checkoutMember, + checkoutDate, + dueReturnDate, + availability, + book: this.book, + }; + } + + updateCheckoutStatus(bookID, newStatus, checkoutDate, checkoutMember, newReturnDate) { + + const record = bookRecordDatabase[bookID]; + record.availability = newStatus; + record.checkoutDate = checkoutDate; + record.checkoutMember = checkoutMember; + record.dueReturnDate = newReturnDate; + } + + extendCheckoutPeriod(bookID, newReturnDate) { + bookRecordDatabase[bookID].dueReturnDate = newReturnDate; + } + + isPastDue(bookID) { + const currentDate = new Date(); + return currentDate.getTime() > Date.parse(bookRecordDatabase[bookID].dueReturnDate); + } +} + +//*******************************************************// +// The Flyweight pattern and the DOM +//*******************************************************// +//********************** Snippet 1 **********************// + +
+
More Info (Address) + + This is more information +
+
Even More Info (Map) + + + +
+
+ +//********************** Snippet 2 **********************// + +var stateManager = { + + fly: function () { + + var self = this; + + $( "#container" ) + .unbind() + .on( "click", "div.toggle", function ( e ) { + self.handleClick( e.target ); + }); + }, + + handleClick: function ( elem ) { + $( elem ).find( "span" ).toggle( "slow" ); + } + }; + +//********************** Snippet 3 **********************// + + +$("div").on( "click", function () { + console.log( "You clicked: " + $( this ).attr( "id" )); + }); + + // we should avoid using the DOM element to create a + // jQuery object (with the overhead that comes with it) + // and just use the DOM element itself like this: + + $( "div" ).on( "click", function () { + console.log( "You clicked:" + this.id ); + }); + + +//********************** Snippet 4 **********************// +$( "a" ).map( function () { + return $( this ).text(); + }); +//********************** Snippet 5 **********************// + +jQuery.single = (function( o ){ + + var collection = jQuery([1]); + return function( element ) { + + // Give collection the element: + collection[0] = element; + + // Return the collection: + return collection; + + }; + })(); + + //********************** Snippet 6 **********************// + $( "div" ).on( "click", function () { + + var html = jQuery.single( this ).next().html(); + console.log( html ); + + }); diff --git a/book/snippets/01-JavaScript-Design-Patterns/2-the-module-pattern.es2015-2.js b/book/snippets/01-JavaScript-Design-Patterns/2-the-module-pattern.es2015-2.js new file mode 100644 index 00000000..72af73a0 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/2-the-module-pattern.es2015-2.js @@ -0,0 +1,153 @@ +//*******************************************************// +// The Module Pattern +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] We have new pattern implementation with WeakMap() +// [ES2015+] The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced. +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration + +let _counter = new WeakMap(); + +class Module { + constructor() { + _counter.set(this, 0); + } + incrementCounter() { + let counter = _counter.get(this); + counter++; + _counter.set(this, counter); + + return _counter.get(this); + } + resetCounter() { + console.log(`counter value prior to reset: ${_counter.get(this)}`); + _counter.set(this, 0); + } +} + +const testModule = new Module(); + +// Usage: + +// Increment our counter +testModule.incrementCounter(); +// Check the counter value and reset +// Outputs: counter value prior to reset: 1 +testModule.resetCounter(); + +//********************** Snippet 2 **********************// + +// [ES2015+] We have new pattern implementation with WeakMap() +// [ES2015+] The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced. +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration + +const myPrivateVar = new WeakMap(); +const myPrivateMethod = new WeakMap(); + +class MyNamespace { + constructor() { + // A private counter variable + myPrivateVar.set(this, 0); + // A private function which logs any arguments + myPrivateMethod.set(this, foo => console.log(foo)); + // A public variable + this.myPublicVar = 'foo'; + } + // A public function utilizing privates + myPublicFunction(bar) { + let privateVar = myPrivateVar.get(this); + const privateMethod = myPrivateMethod.get(this); + // Increment our private counter + privateVar++; + myPrivateVar.set(this, privateVar); + // Call our private method using bar + privateMethod(bar); + } +} + +//********************** Snippet 3 **********************// +// [ES2015+] We have new pattern implementation with WeakMap() +// [ES2015+] The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced. +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration + +const basket = new WeakMap(); +const doSomethingPrivate = new WeakMap(); +const doSomethingElsePrivate = new WeakMap(); + +class BasketModule { + constructor() { + // privates + basket.set(this, []); + doSomethingPrivate.set(this, () => { + //... + }); + doSomethingElsePrivate.set(this, () => { + //... + }); + } + // Public aliases to a private functions + doSomething() { + doSomethingPrivate.get(this)(); + } + doSomethingElse() { + doSomethingElsePrivate.get(this)(); + } + // Add items to our basket + addItem(values) { + const basketData = basket.get(this); + basketData.push(values); + basket.set(this, basketData); + } + // Get the count of items in the basket + getItemCount() { + return basket.get(this).length; + } + // Get the total value of items in the basket + getTotal() { + return basket + .get(this) + .reduce((currentSum, item) => item.price + currentSum, 0); + } +} + +//********************** Snippet 4 **********************// + +// basketModule returns an object with a public API we can use +const basketModule = new BasketModule(); + +basketModule.addItem({ + item: 'bread', + price: 0.5, +}); + +basketModule.addItem({ + item: 'butter', + price: 0.3, +}); + +// Outputs: 2 +console.log(basketModule.getItemCount()); + +// Outputs: 0.8 +console.log(basketModule.getTotal()); + +// However, the following will not work: + +// Outputs: undefined +// This is because the basket itself is not exposed as a part of our +// public API +console.log(basketModule.basket); + +// This also won't work as it only exists within the scope of our +// basketModule closure, but not in the returned public object +console.log(basket); diff --git a/book/snippets/01-JavaScript-Design-Patterns/2-the-module-pattern.es2015.js b/book/snippets/01-JavaScript-Design-Patterns/2-the-module-pattern.es2015.js new file mode 100644 index 00000000..616c1d29 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/2-the-module-pattern.es2015.js @@ -0,0 +1,419 @@ +//*******************************************************// +// Object Literals +//*******************************************************// + +//********************** Snippet 1 **********************// + +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new method declaration +const myObjectLiteral = { + + variableKey: variableValue, + + functionKey() { + // ... + } +}; +//********************** Snippet 2 **********************// + +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new method declaration +const myModule = { + myProperty: 'someValue', + + // object literals can contain properties and methods. + // e.g we can define a further object for module configuration: + myConfig: { + useCaching: true, + language: 'en', + }, + + // a very basic method + saySomething() { + console.log('Where in the world is Paul Irish today?'); + }, + + // output a value based on the current configuration + // [ES2015+] We used new template literals for string interpolation + reportMyConfig() { + console.log( + `Caching is: ${this.myConfig.useCaching ? 'enabled' : 'disabled'}` + ); + }, + + // override the current configuration + updateMyConfig(newConfig) { + if (typeof newConfig === 'object') { + this.myConfig = newConfig; + console.log(this.myConfig.language); + } + }, + }; + + // Outputs: Where in the world is Paul Irish today? + myModule.saySomething(); + + // Outputs: Caching is: enabled + myModule.reportMyConfig(); + + // Outputs: fr + myModule.updateMyConfig({ + language: 'fr', + useCaching: false, + }); + + // Outputs: Caching is: disabled + myModule.reportMyConfig(); + +//*******************************************************// +// The Module Pattern +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] We have new pattern implementation with new keywords import and export +// [ES2015+] The import statement is used to import bindings which are exported by another module. +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new keyword const for immutable constant declaration + +let counter = 0; + +const testModule = { + incrementCounter() { + return counter++; + }, + resetCounter() { + console.log(`counter value prior to reset: ${counter}`); + counter = 0; + }, +}; + +// [ES2015+] Default export module, without name +export default testModule; + +// Usage: + +// [ES2015+] Import module from path +import testModule from './testModule'; + +// Increment our counter +testModule.incrementCounter(); + +// Check the counter value and reset +// Outputs: counter value prior to reset: 1 +testModule.resetCounter(); + + +//********************** Snippet 2 **********************// + +// [ES2015+] We have new pattern implementation with new keywords import and export +// [ES2015+] The import statement is used to import bindings which are exported by another module. +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new keyword const for immutable constant declaration + +// A private counter variable +let myPrivateVar = 0; + +// A private function which logs any arguments +// [ES2015+] Parentheses are optional when there is only one parameter +const myPrivateMethod = foo => { + console.log(foo); +}; + +const myNamespace = { + // A public variable + myPublicVar: 'foo', + + // A public function utilizing privates + myPublicFunction(bar) { + // Increment our private counter + myPrivateVar++; + + // Call our private method using bar + myPrivateMethod(bar); + }, +}; + +// [ES2015+] Default export module, without name +export default myNamespace; + + +//********************** Snippet 3 **********************// +// [ES2015+] We have new pattern implementation with new keywords import and export +// [ES2015+] The import statement is used to import bindings which are exported by another module. +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new keyword const for immutable constant declaration + +// privates + +const basket = []; + +const doSomethingPrivate = () => { + //... +}; + +const doSomethingElsePrivate = () => { + //... +}; + +// Create an object exposed to the public +const basketModule = { + // Add items to our basket + addItem(values) { + basket.push(values); + }, + + // Get the count of items in the basket + getItemCount() { + return basket.length; + }, + + // Public alias to a private function + doSomething() { + doSomethingPrivate(); + }, + + // Get the total value of items in the basket + // [ES2015+] The reduce() method applies a function against an accumulator and each element in the array (from left to right) to reduce it to a single value. + getTotal() { + return basket.reduce((currentSum, item) => item.price + currentSum, 0); + }, +}; +// [ES2015+] Default export module, without name +export default basketModule; + +//********************** Snippet 4 **********************// +// [ES2015+] Import module from path +import basketModule from './basketModule'; + +// basketModule returns an object with a public API we can use + +basketModule.addItem({ + item: 'bread', + price: 0.5, +}); + +basketModule.addItem({ + item: 'butter', + price: 0.3, +}); + +// Outputs: 2 +console.log(basketModule.getItemCount()); + +// Outputs: 0.8 +console.log(basketModule.getTotal()); + +// However, the following will not work: + +// Outputs: undefined +// This is because the basket itself is not exposed as a part of our +// public API +console.log(basketModule.basket); + +// This also won't work as it only exists within the scope of our +// basketModule closure, but not in the returned public object +console.log(basket); + + +//*******************************************************// +// Module Pattern Variations +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] We have new pattern implementation with new keywords import and export +// [ES2015+] The import statement is used to import bindings which are exported by another module. +// [ES2015+] We used new keyword const for immutable constant declaration + +const privateMethod1 = () => { + $(".container").html("test"); +} + +const privateMethod2 = () => { + console.log(_.min([10, 5, 100, 2, 1000])); +} + +const myModule = { + publicMethod() { + privateMethod1(); + } +}; + +// [ES2015+] Default export module, without name +export default myModule; + + +// [ES2015+] Import module from path +import myModule from './MyModule'; + +myModule.publicMethod(); + +//********************** Snippet 2 **********************// +// [ES2015+] We have new pattern implementation with new keywords import and export +// [ES2015+] The import statement is used to import bindings which are exported by another module. + +// Module object +const module = {}; +const privateVariable = 'Hello World'; + +const privateMethod = () => { + // ... +}; + +module.publicProperty = 'Foobar'; +module.publicMethod = () => { + console.log(privateVariable); +}; + +// [ES2015+] Default export module, without name +export default module; + + +//*******************************************************// +// Toolkit And Framework-specific Module Pattern Implementations +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration + +const store = window.store || {}; + +if (!store['basket']) { + store.basket = {}; +} + +if (!store.basket['core']) { + store.basket.core = {}; +} + +store.basket.core = { + // ...rest of our logic +}; + +//********************** Snippet 2 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax + +require(['dojo/_base/customStore'], store => { + // using dojo.setObject() + store.setObject( + 'basket.core', + (() => { + const basket = []; + + const privateMethod = () => { + console.log(basket); + }; + + return { + publicMethod() { + privateMethod(); + }, + }; + })() + ); +}); + + +//********************** Snippet 3 **********************// +// [ES2015+] We used new arrow function syntax +// [ES2015+] We used new keyword const for immutable constant declaration +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new template literals for string interpolation + +// create namespace +Ext.namespace('myNameSpace'); + +// create application +myNameSpace.app = (() => { + // do NOT access DOM from here; elements don't exist yet + + // private variables + let btn1; + const privVar1 = 11; + + // private functions + const btn1Handler = (button, event) => { + console.log(`privVar1=${privVar1}`); + console.log(`this.btn1Text=${this.btn1Text}`); + }; + + // public space + return { + // public properties, e.g. strings to translate + btn1Text: 'Button 1', + + // public methods + init() { + if (Ext.Ext2) { + btn1 = new Ext.Button({ + renderTo: 'btn1-ct', + text: this.btn1Text, + handler: btn1Handler, + }); + } else { + btn1 = new Ext.Button('btn1-ct', { + text: this.btn1Text, + handler: btn1Handler, + }); + } + }, + }; +})(); + + +//********************** Snippet 4 **********************// +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new arrow function syntax + +Y.namespace('store.basket'); +Y.store.basket = (() => { + let myPrivateVar; + let myPrivateMethod; + + // private variables: + myPrivateVar = 'I can be accessed only within Y.store.basket.'; + + // private method: + myPrivateMethod = () => { + Y.log('I can be accessed only from within YAHOO.store.basket'); + }; + + return { + myPublicProperty: "I'm a public property.", + + myPublicMethod() { + Y.log("I'm a public method."); + + // Within basket, I can access "private" vars and methods: + Y.log(myPrivateVar); + Y.log(myPrivateMethod()); + + // The native scope of myPublicMethod is store so we can + // access public members using "this": + Y.log(this.myPublicProperty); + }, + }; +})(); + +//********************** Snippet 5 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax + +const library = module => { + $(() => { + if (module.init) { + module.init(); + } + }); + + return module; +}; + +const myLibrary = library( + (() => ({ + init() { + // module implementation + }, + }))() +); + diff --git a/book/snippets/01-JavaScript-Design-Patterns/2-the-module-pattern.es5.js b/book/snippets/01-JavaScript-Design-Patterns/2-the-module-pattern.es5.js new file mode 100644 index 00000000..d1fa1d96 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/2-the-module-pattern.es5.js @@ -0,0 +1,402 @@ +//*******************************************************// +// Object Literals +//*******************************************************// + +//********************** Snippet 1 **********************// + +var myObjectLiteral = { + + variableKey: variableValue, + + functionKey: function () { + // ... + } +}; + +//********************** Snippet 2 **********************// + +var myModule = { + + myProperty: "someValue", + + // object literals can contain properties and methods. + // e.g we can define a further object for module configuration: + myConfig: { + useCaching: true, + language: "en" + }, + + // a very basic method + saySomething: function () { + console.log( "Where in the world is Paul Irish today?" ); + }, + + // output a value based on the current configuration + reportMyConfig: function () { + console.log( "Caching is: " + ( this.myConfig.useCaching ? "enabled" : "disabled") ); + }, + + // override the current configuration + updateMyConfig: function( newConfig ) { + + if ( typeof newConfig === "object" ) { + this.myConfig = newConfig; + console.log( this.myConfig.language ); + } + } +}; + +// Outputs: Where in the world is Paul Irish today? +myModule.saySomething(); + +// Outputs: Caching is: enabled +myModule.reportMyConfig(); + +// Outputs: fr +myModule.updateMyConfig({ + language: "fr", + useCaching: false +}); + +// Outputs: Caching is: disabled +myModule.reportMyConfig(); + +//*******************************************************// +// The Module Pattern +//*******************************************************// + +//********************** Snippet 1 **********************// + +var testModule = (function () { + + var counter = 0; + + return { + + incrementCounter: function () { + return counter++; + }, + + resetCounter: function () { + console.log( "counter value prior to reset: " + counter ); + counter = 0; + } + }; + +})(); + +// Usage: + +// Increment our counter +testModule.incrementCounter(); + +// Check the counter value and reset +// Outputs: counter value prior to reset: 1 +testModule.resetCounter(); + +//********************** Snippet 2 **********************// + +var myNamespace = (function () { + + var myPrivateVar, myPrivateMethod; + + // A private counter variable + myPrivateVar = 0; + + // A private function which logs any arguments + myPrivateMethod = function( foo ) { + console.log( foo ); + }; + + return { + + // A public variable + myPublicVar: "foo", + + // A public function utilizing privates + myPublicFunction: function( bar ) { + + // Increment our private counter + myPrivateVar++; + + // Call our private method using bar + myPrivateMethod( bar ); + + } + }; + +})(); + +//********************** Snippet 3 **********************// + +var basketModule = (function () { + + // privates + + var basket = []; + + function doSomethingPrivate() { + //... + } + + function doSomethingElsePrivate() { + //... + } + + // Return an object exposed to the public + return { + + // Add items to our basket + addItem: function( values ) { + basket.push(values); + }, + + // Get the count of items in the basket + getItemCount: function () { + return basket.length; + }, + + // Public alias to a private function + doSomething: doSomethingPrivate, + + // Get the total value of items in the basket + getTotal: function () { + + var q = this.getItemCount(), + p = 0; + + while (q--) { + p += basket[q].price; + } + + return p; + } + }; +})(); + +//********************** Snippet 4 **********************// + +// basketModule returns an object with a public API we can use + +basketModule.addItem({ + item: "bread", + price: 0.5 +}); + +basketModule.addItem({ + item: "butter", + price: 0.3 +}); + +// Outputs: 2 +console.log( basketModule.getItemCount() ); + +// Outputs: 0.8 +console.log( basketModule.getTotal() ); + +// However, the following will not work: + +// Outputs: undefined +// This is because the basket itself is not exposed as a part of our +// public API +console.log( basketModule.basket ); + +// This also won't work as it only exists within the scope of our +// basketModule closure, but not in the returned public object +console.log( basket ); + +//*******************************************************// +// Module Pattern Variations +//*******************************************************// + +//********************** Snippet 1 **********************// + +// Global module +var myModule = (function ( jQ, _ ) { + + function privateMethod1(){ + jQ(".container").html("test"); + } + + function privateMethod2(){ + console.log( _.min([10, 5, 100, 2, 1000]) ); + } + + return{ + publicMethod: function(){ + privateMethod1(); + } + }; + +// Pull in jQuery and Underscore +})( jQuery, _ ); + +myModule.publicMethod(); + +//********************** Snippet 2 **********************// + +// Global module +var myModule = (function () { + + // Module object + var module = {}, + privateVariable = "Hello World"; + + function privateMethod() { + // ... + } + + module.publicProperty = "Foobar"; + module.publicMethod = function () { + console.log( privateVariable ); + }; + + return module; + +})(); + +//*******************************************************// +// Toolkit And Framework-specific Module Pattern Implementations +//*******************************************************// + +//********************** Snippet 1 **********************// + +var store = window.store || {}; + +if ( !store["basket"] ) { + store.basket = {}; +} + +if ( !store.basket["core"] ) { + store.basket.core = {}; +} + +store.basket.core = { + // ...rest of our logic +}; + +//********************** Snippet 2 **********************// + +require(["dojo/_base/customStore"], function( store ){ + + // using dojo.setObject() + store.setObject( "basket.core", (function() { + + var basket = []; + + function privateMethod() { + console.log(basket); + } + + return { + publicMethod: function(){ + privateMethod(); + } + }; + + })()); + +}); + +//********************** Snippet 3 **********************// + +// create namespace +Ext.namespace("myNameSpace"); + +// create application +myNameSpace.app = function () { + + // do NOT access DOM from here; elements don't exist yet + + // private variables + var btn1, + privVar1 = 11; + + // private functions + var btn1Handler = function ( button, event ) { + console.log( "privVar1=" + privVar1 ); + console.log( "this.btn1Text=" + this.btn1Text ); + }; + + // public space + return { + // public properties, e.g. strings to translate + btn1Text: "Button 1", + + // public methods + init: function () { + + if ( Ext.Ext2 ) { + + btn1 = new Ext.Button({ + renderTo: "btn1-ct", + text: this.btn1Text, + handler: btn1Handler + }); + + } else { + + btn1 = new Ext.Button( "btn1-ct", { + text: this.btn1Text, + handler: btn1Handler + }); + + } + } + }; +}(); + +//********************** Snippet 4 **********************// + +Y.namespace( "store.basket" ) ; +Y.store.basket = (function () { + + var myPrivateVar, myPrivateMethod; + + // private variables: + myPrivateVar = "I can be accessed only within Y.store.basket."; + + // private method: + myPrivateMethod = function () { + Y.log( "I can be accessed only from within YAHOO.store.basket" ); + } + + return { + myPublicProperty: "I'm a public property.", + + myPublicMethod: function () { + Y.log( "I'm a public method." ); + + // Within basket, I can access "private" vars and methods: + Y.log( myPrivateVar ); + Y.log( myPrivateMethod() ); + + // The native scope of myPublicMethod is store so we can + // access public members using "this": + Y.log( this.myPublicProperty ); + } + }; + +})(); + +//********************** Snippet 5 **********************// + +function library( module ) { + + $( function() { + if ( module.init ) { + module.init(); + } + }); + + return module; +} + +var myLibrary = library(function () { + + return { + init: function () { + // module implementation + } + }; +}()); \ No newline at end of file diff --git a/book/snippets/01-JavaScript-Design-Patterns/3-the-revealing-module-pattern.es2015.js b/book/snippets/01-JavaScript-Design-Patterns/3-the-revealing-module-pattern.es2015.js new file mode 100644 index 00000000..aa8b828d --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/3-the-revealing-module-pattern.es2015.js @@ -0,0 +1,80 @@ +//********************** Snippet 1 **********************// +// [ES2015+] We used new template literals for string interpolation +// [ES2015+] We used new keyword const for immutable constant declaration +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new arrow function syntax +// [ES2015+] We have new pattern implementation with new keywords import and export + +let privateVar = 'Ben Cherry'; +const publicVar = 'Hey there!'; + +const privateFunction = () => { + console.log(`Name:${privateVar}`); +}; + +// [ES2015+] Parentheses are optional when there's only one parameter +const publicSetName = strName => { + privateVar = strName; +}; + +const publicGetName = () => { + privateFunction(); +}; + +// Reveal public pointers to +// private functions and properties +const myRevealingModule = { + setName: publicSetName, + greeting: publicVar, + getName: publicGetName, +}; + +// [ES2015+] Default export module, without name +export default myRevealingModule; + +// Usage: +// [ES2015+] The import statement is used to import bindings which are exported by another module. +import myRevealingModule from './myRevealingModule'; + +myRevealingModule.setName('Paul Kinlan'); + + +//********************** Snippet 2 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new arrow function syntax +// [ES2015+] We have new pattern implementation with new keywords import and export + +let privateCounter = 0; + +const privateFunction = () => { + privateCounter++; +} + +const publicFunction = () => { + publicIncrement(); +} + +const publicIncrement = () => { + privateFunction(); +} + +// [ES2015+] Equivalent to: => { return privateCounter; } +const publicGetCount = () => privateCounter; + +// Reveal public pointers to +// private functions and properties +const myRevealingModule = { + start: publicFunction, + increment: publicIncrement, + count: publicGetCount +}; + +// [ES2015+] Default export module, without name +export default myRevealingModule; + +// Usage: +// [ES2015+] The import statement is used to import bindings which are exported by another module. +import myRevealingModule from './myRevealingModule'; + +myRevealingModule.start(); diff --git a/book/snippets/01-JavaScript-Design-Patterns/3-the-revealing-module-pattern.es5.js b/book/snippets/01-JavaScript-Design-Patterns/3-the-revealing-module-pattern.es5.js new file mode 100644 index 00000000..ba163afd --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/3-the-revealing-module-pattern.es5.js @@ -0,0 +1,68 @@ +//********************** Snippet 1 **********************// + +var myRevealingModule = (function () { + + var privateVar = "Ben Cherry", + publicVar = "Hey there!"; + + function privateFunction() { + console.log( "Name:" + privateVar ); + } + + function publicSetName( strName ) { + privateVar = strName; + } + + function publicGetName() { + privateFunction(); + } + + + // Reveal public pointers to + // private functions and properties + + return { + setName: publicSetName, + greeting: publicVar, + getName: publicGetName + }; + +})(); + +myRevealingModule.setName( "Paul Kinlan" ); + +//********************** Snippet 2 **********************// + +var myRevealingModule = (function () { + + var privateCounter = 0; + + function privateFunction() { + privateCounter++; + } + + function publicFunction() { + publicIncrement(); + } + + function publicIncrement() { + privateFunction(); + } + + function publicGetCount(){ + return privateCounter; + } + + // Reveal public pointers to + // private functions and properties + + return { + start: publicFunction, + increment: publicIncrement, + count: publicGetCount + }; + +})(); + +myRevealingModule.start(); + diff --git a/book/snippets/01-JavaScript-Design-Patterns/4-the-singleton-pattern.es2015.js b/book/snippets/01-JavaScript-Design-Patterns/4-the-singleton-pattern.es2015.js new file mode 100644 index 00000000..060dfc47 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/4-the-singleton-pattern.es2015.js @@ -0,0 +1,149 @@ +//********************** Snippet 1 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new arrow function syntax +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance + +// [ES2015+] We have new pattern implementation with new keywords import and export + +// Instance stores a reference to the Singleton +let instance; + +// Private methods and variables +const privateMethod = () => { + console.log('I am private'); + }; +const privateVariable = 'Im also private'; +const randomNumber = Math.random(); + +// Singleton +class MySingleton { + // Get the Singleton instance if one exists + // or create one if it doesn't + constructor() { + if (!instance) { + // Public property + this.publicProperty = 'I am also public'; + instance = this; + } + + return instance; + } + + // Public methods + publicMethod() { + console.log('The public can see me!'); + } + + getRandomNumber() { + return randomNumber; + } +} +// [ES2015+] Default export module, without name +export default MySingleton; + + +// Instance stores a reference to the Singleton +let instance; + +// Singleton +class MyBadSingleton { + // Always create a new Singleton instance + constructor() { + this.randomNumber = Math.random(); + instance = this; + + return instance; + } + + getRandomNumber() { + return this.randomNumber; + } +} + +// [ES2015+] Default export module, without name +export default MyBadSingleton; + + +// Usage: +// [ES2015+] The import statement is used to import bindings which are exported by another module. +import MySingleton from './MySingleton'; +import MyBadSingleton from './MyBadSingleton'; + +const singleA = new MySingleton(); +const singleB = new MySingleton(); +console.log(singleA.getRandomNumber() === singleB.getRandomNumber()); // true + +const badSingleA = new MyBadSingleton(); +const badSingleB = new MyBadSingleton(); +console.log(badSingleA.getRandomNumber() !== badSingleB.getRandomNumber()); // true + +// Note: as we are working with random numbers, there is a +// mathematical possibility both numbers will be the same, +// however unlikely. The above example should otherwise still +// be valid. + +//********************** Snippet 2 **********************// +// [ES2015+] We used new constructor method + +constructor() { + if (this._instance == null) { + if (isFoo()) { + this._instance = new FooSingleton(); + } else { + this._instance = new BasicSingleton(); + } + } + + return this._instance; +} + +//********************** Snippet 3 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new arrow function syntax +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance + +// options: an object containing configuration options for the singleton +// e.g const options = { name: "test", pointX: 5}; +class Singleton { + constructor(options) { + // set options to the options supplied + // or an empty object if none are provided + options = options || {}; + // set some properties for our singleton + this.name = 'SingletonTester'; + this.pointX = options.pointX || 6; + this.pointY = options.pointY || 10; + } + } + + // our instance holder + let instance; + + // an emulation of static variables and methods + const SingletonTester = { + name: 'SingletonTester', + // Method for getting an instance. It returns + // a singleton instance of a singleton object + getInstance(options) { + if (instance === undefined) { + instance = new Singleton(options); + } + + return instance; + }, + }; + + const singletonTest = SingletonTester.getInstance({ + pointX: 5, + }); + + // Log the output of pointX just to verify it is correct + // Outputs: 5 + console.log(singletonTest.pointX); + \ No newline at end of file diff --git a/book/snippets/01-JavaScript-Design-Patterns/4-the-singleton-pattern.es5.js b/book/snippets/01-JavaScript-Design-Patterns/4-the-singleton-pattern.es5.js new file mode 100644 index 00000000..e89ba1d6 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/4-the-singleton-pattern.es5.js @@ -0,0 +1,171 @@ +//********************** Snippet 1 **********************// + +var mySingleton = (function () { + + // Instance stores a reference to the Singleton + var instance; + + function init() { + + // Singleton + + // Private methods and variables + function privateMethod(){ + console.log( "I am private" ); + } + + var privateVariable = "Im also private"; + + var privateRandomNumber = Math.random(); + + return { + + // Public methods and variables + publicMethod: function () { + console.log( "The public can see me!" ); + }, + + publicProperty: "I am also public", + + getRandomNumber: function() { + return privateRandomNumber; + } + + }; + + }; + + return { + + // Get the Singleton instance if one exists + // or create one if it doesn't + getInstance: function () { + + if ( !instance ) { + instance = init(); + } + + return instance; + } + + }; + + })(); + + var myBadSingleton = (function () { + + // Instance stores a reference to the Singleton + var instance; + + function init() { + + // Singleton + + var privateRandomNumber = Math.random(); + + return { + + getRandomNumber: function() { + return privateRandomNumber; + } + + }; + + }; + + return { + + // Always create a new Singleton instance + getInstance: function () { + + instance = init(); + + return instance; + } + + }; + + })(); + + + // Usage: + + var singleA = mySingleton.getInstance(); + var singleB = mySingleton.getInstance(); + console.log( singleA.getRandomNumber() === singleB.getRandomNumber() ); // true + + var badSingleA = myBadSingleton.getInstance(); + var badSingleB = myBadSingleton.getInstance(); + console.log( badSingleA.getRandomNumber() !== badSingleB.getRandomNumber() ); // true + + // Note: as we are working with random numbers, there is a + // mathematical possibility both numbers will be the same, + // however unlikely. The above example should otherwise still + // be valid. + + //********************** Snippet 2 **********************// + + mySingleton.getInstance = function(){ + if ( this._instance == null ) { + if ( isFoo() ) { + this._instance = new FooSingleton(); + } else { + this._instance = new BasicSingleton(); + } + } + return this._instance; + }; + + + //********************** Snippet 3 **********************// + + var SingletonTester = (function () { + + // options: an object containing configuration options for the singleton + // e.g var options = { name: "test", pointX: 5}; + function Singleton( options ) { + + // set options to the options supplied + // or an empty object if none are provided + options = options || {}; + + // set some properties for our singleton + this.name = "SingletonTester"; + + this.pointX = options.pointX || 6; + + this.pointY = options.pointY || 10; + + } + + // our instance holder + var instance; + + // an emulation of static variables and methods + var _static = { + + name: "SingletonTester", + + // Method for getting an instance. It returns + // a singleton instance of a singleton object + getInstance: function( options ) { + if( instance === undefined ) { + instance = new Singleton( options ); + } + + return instance; + + } + }; + + return _static; + + })(); + + var singletonTest = SingletonTester.getInstance({ + pointX: 5 + }); + + // Log the output of pointX just to verify it is correct + // Outputs: 5 + console.log( singletonTest.pointX ); \ No newline at end of file diff --git a/book/snippets/01-JavaScript-Design-Patterns/5-the-observer-pattern.es2015.js b/book/snippets/01-JavaScript-Design-Patterns/5-the-observer-pattern.es2015.js new file mode 100644 index 00000000..37c79767 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/5-the-observer-pattern.es2015.js @@ -0,0 +1,595 @@ +//********************** Snippet 1 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance +// [SE2015+] We used new keyword let, which declares a block scope local variabl + +class ObserverList { + constructor() { + this.observerList = []; + } + + add(obj) { + return this.observerList.push(obj); + } + + count() { + return this.observerList.length; + } + + get(index) { + if (index > -1 && index < this.observerList.length) { + return this.observerList[index]; + } + } + + indexOf(obj, startIndex) { + let i = startIndex; + + while (i < this.observerList.length) { + if (this.observerList[i] === obj) { + return i; + } + i++; + } + + return -1; + } + + removeAt(index) { + this.observerList.splice(index, 1); + } + } + + //********************** Snippet 2 **********************// + // [ES2015+] Below we used new class declaration, using keyword class + // [ES2015+] We used new constructor method and method declaration + // [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance + // [SE2015+] We used new keyword let, which declares a block scope local variabl + + class Subject { + constructor() { + this.observers = new ObserverList(); + } + + addObserver(observer) { + this.observers.add(observer); + } + + removeObserver(observer) { + this.observers.removeAt(this.observers.indexOf(observer, 0)); + } + + notify(context) { + const observerCount = this.observers.count(); + for (let i = 0; i < observerCount; i++) { + this.observers.get(i).update(context); + } + } + } + + +//********************** Snippet 3 **********************// + +// The Observer +class Observer { + constructor() {} + update() { + // ... + } +} + +//********************** Snippet 4 **********************// + + + +
+ +//********************** Snippet 5 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] We have new pattern implementation with new inheritance +// [ES2015+] The extends keyword is used to create a class which is a child of another class. +// [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance +// [ES2015+] We used new keyword const for immutable constant declaration +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new arrow function syntax +// Concrete Subject +class ConcreteSubject extends Subject { + constructor(element) { + // [ES2015+] A constructor can use the super keyword to call the constructor of the super class. + super(); + this.element = element; + + // Clicking the checkbox will trigger notifications to its observers + this.element.onclick = () => { + this.notify(this.element.checked); + }; + } + } + + // Concrete Observer + + class ConcreteObserver extends Observer { + constructor(element) { + super(); + this.element = element; + } + + // Override with custom update behaviour + update(value) { + this.element.checked = value; + } + } + + // References to our DOM elements + const addBtn = document.getElementById('addNewObserver'); + const container = document.getElementById('observersContainer'); + const controlCheckbox = new ConcreteSubject( + document.getElementById('mainCheckbox') + ); + + const addNewObserver = () => { + // Create a new checkbox to be added + const check = document.createElement('input'); + check.type = 'checkbox'; + const checkObserver = new ConcreteObserver(check); + + // Add the new observer to our list of observers + // for our main subject + controlCheckbox.addObserver(checkObserver); + + // Append the item to the container + container.appendChild(check); + }; + + addBtn.onclick = addNewObserver; + +//*******************************************************// +// Differences Between The Observer And Publish/Subscribe Pattern +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new arrow function syntax + +// A very simple new mail handler + +// A count of the number of messages received +let mailCounter = 0; + +// Initialize subscribers that will listen out for a topic +// with the name "inbox/newMessage". + +// Render a preview of new messages +const subscriber1 = subscribe('inbox/newMessage', (topic, data) => { + // Log the topic for debugging purposes + console.log('A new message was received: ', topic); + + // Use the data that was passed from our subject + // to display a message preview to the user + $('.messageSender').html(data.sender); + $('.messagePreview').html(data.body); +}); + +// Here's another subscriber using the same data to perform +// a different task. + +// Update the counter displaying the number of new +// messages received via the publisher + +const subscriber2 = subscribe('inbox/newMessage', (topic, data) => { + $('.newMessageCounter').html(++mailCounter); +}); + +publish('inbox/newMessage', [ + { + sender: 'hello@google.com', + body: 'Hey there! How are you doing today?', + }, +]); + +// We could then at a later point unsubscribe our subscribers +// from receiving any new topic notifications as follows: +// unsubscribe( subscriber1 ); +// unsubscribe( subscriber2 ); + + +//*******************************************************// +// Publish/Subscribe Implementations +//*******************************************************// + +//********************** Snippet 1 **********************// + +// Publish + +// jQuery: $(obj).trigger("channel", [arg1, arg2, arg3]); +$(el).trigger('/login', [ + { + username: 'test', + userData: 'test', + }, + ]); + + // Dojo: dojo.publish("channel", [arg1, arg2, arg3] ); + dojo.publish('/login', [ + { + username: 'test', + userData: 'test', + }, + ]); + + // YUI: el.publish("channel", [arg1, arg2, arg3]); + el.publish('/login', { + username: 'test', + userData: 'test', + }); + +// Subscribe + +// jQuery: $(obj).on( "channel", [data], fn ); +$(el).on('/login', event => {...}); + +// Dojo: dojo.subscribe( "channel", fn); +const handle = dojo.subscribe('/login', data => {..}); + +// YUI: el.on("channel", handler); +el.on('/login', data => {...}); + + +// Unsubscribe + +// jQuery: $(obj).off( "channel" ); +$(el).off('/login'); + +// Dojo: dojo.unsubscribe( handle ); +dojo.unsubscribe(handle); + +// YUI: el.detach("channel"); +el.detach('/login'); + +//********************** Snippet 2 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance +// [ES2015+] We used new keyword const for immutable constant declaration +// [SE2015+] We used new keyword let, which declares a block scope local variable + +class PubSub { + constructor() { + // Storage for topics that can be broadcast + // or listened to + this.topics = {}; + + // A topic identifier + this.subUid = -1; + } + + publish(topic, args) { + if (!this.topics[topic]) { + return false; + } + + const subscribers = this.topics[topic]; + let len = subscribers ? subscribers.length : 0; + + while (len--) { + subscribers[len].func(topic, args); + } + + return this; + } + + subscribe(topic, func) { + if (!this.topics[topic]) { + this.topics[topic] = []; + } + + const token = (++this.subUid).toString(); + this.topics[topic].push({ + token, + func, + }); + return token; + } + + unsubscribe(token) { + for (const m in this.topics) { + if (this.topics[m]) { + for (let i = 0, j = this.topics[m].length; i < j; i++) { + if (this.topics[m][i].token === token) { + this.topics[m].splice(i, 1); + + return token; + } + } + } + } + return this; + } +} + +const pubsub = new PubSub(); + +//********************** Snippet 3 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new template literals for string interpolation +// [ES2015+] We used new arrow function syntax + +// Another simple message handler + +// A simple message logger that logs any topics and data received through our +// subscriber +const messageLogger = (topics, data) => { + console.log(`Logging: ${topics}: ${data}`); +}; + +// Subscribers listen for topics they have subscribed to and +// invoke a callback function (e.g messageLogger) once a new +// notification is broadcast on that topic +const subscription = pubsub.subscribe('inbox/newMessage', messageLogger); + +// Publishers are in charge of publishing topics or notifications of +// interest to the application. e.g: + +pubsub.publish('inbox/newMessage', 'hello world!'); + +// or +pubsub.publish('inbox/newMessage', ['test', 'a', 'b', 'c']); + +// or +pubsub.publish('inbox/newMessage', { + sender: 'hello@google.com', + body: 'Hey again!', +}); + +// We can also unsubscribe if we no longer wish for our subscribers +// to be notified +pubsub.unsubscribe(subscription); + +// Once unsubscribed, this for example won't result in our +// messageLogger being executed as the subscriber is +// no longer listening +pubsub.publish('inbox/newMessage', 'Hello! are you still there?'); + +//********************** Snippet 4 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new template literals for string interpolation +// [ES2015+] We used new arrow function syntax + +// Return the current local time to be used in our UI later +getCurrentTime = () => { + const date = new Date(); + const m = date.getMonth() + 1; + const d = date.getDate(); + const y = date.getFullYear(); + const t = date.toLocaleTimeString().toLowerCase(); + + return `${m}/${d}/${y} ${t}`; + }; + + // Add a new row of data to our fictional grid component + const addGridRow = data => { + // ui.grid.addRow( data ); + console.log(`updated grid component with:${data}`); + }; + + // Update our fictional grid to show the time it was last + // updated + const updateCounter = data => { + // ui.grid.updateLastChanged( getCurrentTime() ); + console.log(`data last updated at: ${getCurrentTime()} with ${data}`); + }; + + // Update the grid using the data passed to our subscribers + const gridUpdate = (topic, data) => { + if (data !== undefined) { + addGridRow(data); + updateCounter(data); + } + }; + + +// Create a subscription to the newDataAvailable topic +const subscriber = pubsub.subscribe('newDataAvailable', gridUpdate); + +// The following represents updates to our data layer. This could be +// powered by ajax requests which broadcast that new data is available +// to the rest of the application. + +// Publish changes to the gridUpdated topic representing new entries +pubsub.publish('newDataAvailable', { + summary: 'Apple made $5 billion', + identifier: 'APPL', + stockPrice: 570.91, +}); + +pubsub.publish('newDataAvailable', { + summary: 'Microsoft made $20 million', + identifier: 'MSFT', + stockPrice: 30.85, +}); + +//********************** Snippet 5 **********************// + + + + + + +
+ +
+

+ + +

+

+ + +

+

+ + + +

+

+ + +

+
+ + + +
+

Recent users

+

Recent movies rated

+
+ +
+ +//********************** Snippet 6 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax + +;($ => { + // Pre-compile templates and "cache" them using closure + const userTemplate = _.template($('#userTemplate').html()); + + const ratingsTemplate = _.template($('#ratingsTemplate').html()); + + // Subscribe to the new user topic, which adds a user + // to a list of users who have submitted reviews + $.subscribe('/new/user', (e, data) => { + if (data) { + $('#users').append(userTemplate(data)); + } + }); + + // Subscribe to the new rating topic. This is composed of a title and + // rating. New ratings are appended to a running list of added user + // ratings. + $.subscribe('/new/rating', (e, data) => { + if (data) { + $('#ratings').append(ratingsTemplate(data)); + } + }); + + // Handler for adding a new user + // [ES2015+] Parentheses are optional when there is only one parameter + $('#add').on('click', e => { + e.preventDefault(); + + const strUser = $('#twitter_handle').val(); + const strMovie = $('#movie_seen').val(); + const strRating = $('#movie_rating').val(); + + // Inform the application a new user is available + $.publish('/new/user', { + name: strUser, + }); + + // Inform the app a new rating is available + $.publish('/new/rating', { + title: strMovie, + rating: strRating, + }); + }); + })(jQuery); + + //********************** Snippet 7 **********************// + +
+ + + + + +
+ + + +
+ +
    + + + + + +//********************** Snippet 8 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax +// [ES2015+] We used new template literals for string interpolation + +;($ => { + // Pre-compile template and "cache" it using closure + const resultTemplate = _.template($('#resultTemplate').html()); + + // Subscribe to the new search tags topic + $.subscribe('/search/tags', (e, tags) => { + $('#lastQuery').html(`

    Searched for:${tags}

    `); + }); + + // Subscribe to the new results topic + $.subscribe('/search/resultSet', (e, results) => { + $('#searchResults') + .empty() + .append(resultTemplate(results)); + }); + + // Submit a search query and publish tags on the /search/tags topic + $('#flickrSearch').submit(function(e) { + e.preventDefault(); + const tags = $(this) + .find('#query') + .val(); + + if (!tags) { + return; + } + + $.publish('/search/tags', [$.trim(tags)]); + }); + + // Subscribe to new tags being published and perform + // a search query using them. Once data has returned + // publish this data for the rest of the application + // to consume + // [ES2015+] We used the destructuring assignment syntax that makes it possible to unpack values from data structures into distinct variables. + $.subscribe('/search/tags', (e, tags) => { + $.getJSON( + 'http://api.flickr.com/services/feeds/photos_public.gne?jsoncallback=?', + { + tags, + tagmode: 'any', + format: 'json', + }, + // [ES2015+] The destructuring assignment as function parameter + ({ items }) => { + if (!items.length) { + return; + } + // [ES2015+] New shorthand property names in object creation, if variable name equal to object key + $.publish('/search/resultSet', { items }); + } + ); + }); + })(jQuery); + \ No newline at end of file diff --git a/book/snippets/01-JavaScript-Design-Patterns/5-the-observer-pattern.es5.js b/book/snippets/01-JavaScript-Design-Patterns/5-the-observer-pattern.es5.js new file mode 100644 index 00000000..957cda44 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/5-the-observer-pattern.es5.js @@ -0,0 +1,572 @@ +//********************** Snippet 1 **********************// + +function ObserverList() { + this.observerList = []; +} + +ObserverList.prototype.add = function (obj) { + return this.observerList.push(obj); +}; + +ObserverList.prototype.count = function () { + return this.observerList.length; +}; + +ObserverList.prototype.get = function (index) { + if (index > -1 && index < this.observerList.length) { + return this.observerList[index]; + } +}; + +ObserverList.prototype.indexOf = function (obj, startIndex) { + var i = startIndex; + + while (i < this.observerList.length) { + if (this.observerList[i] === obj) { + return i; + } + i++; + } + + return -1; +}; + +ObserverList.prototype.removeAt = function (index) { + this.observerList.splice(index, 1); +}; + +//********************** Snippet 2 **********************// + +function Subject() { + this.observers = new ObserverList(); +} + +Subject.prototype.addObserver = function (observer) { + this.observers.add(observer); +}; + +Subject.prototype.removeObserver = function (observer) { + this.observers.removeAt(this.observers.indexOf(observer, 0)); +}; + +Subject.prototype.notify = function (context) { + var observerCount = this.observers.count(); + for (var i = 0; i < observerCount; i++) { + this.observers.get(i).update(context); + } +}; + +//********************** Snippet 3 **********************// + +// The Observer +function Observer() { + this.update = function () { + // ... + }; +} + +//********************** Snippet 4 **********************// + + + +
    + +//********************** Snippet 5 **********************// + +// Extend an object with an extension +function extend( obj, extension ){ + for ( var key in extension ){ + obj[key] = extension[key]; + } + } + + // References to our DOM elements + + var controlCheckbox = document.getElementById( "mainCheckbox" ), + addBtn = document.getElementById( "addNewObserver" ), + container = document.getElementById( "observersContainer" ); + + + // Concrete Subject + + // Extend the controlling checkbox with the Subject class + extend( controlCheckbox, new Subject() ); + + // Clicking the checkbox will trigger notifications to its observers + controlCheckbox.onclick = function(){ + controlCheckbox.notify( controlCheckbox.checked ); + }; + + addBtn.onclick = addNewObserver; + + // Concrete Observer + + function addNewObserver(){ + + // Create a new checkbox to be added + var check = document.createElement( "input" ); + check.type = "checkbox"; + + // Extend the checkbox with the Observer class + extend( check, new Observer() ); + + // Override with custom update behaviour + check.update = function( value ){ + this.checked = value; + }; + + // Add the new observer to our list of observers + // for our main subject + controlCheckbox.addObserver( check ); + + // Append the item to the container + container.appendChild( check ); + } + +//*******************************************************// +// Differences Between The Observer And Publish/Subscribe Pattern +//*******************************************************// + +//********************** Snippet 1 **********************// + +// A very simple new mail handler + +// A count of the number of messages received +var mailCounter = 0; + +// Initialize subscribers that will listen out for a topic +// with the name "inbox/newMessage". + +// Render a preview of new messages +var subscriber1 = subscribe( "inbox/newMessage", function( topic, data ) { + + // Log the topic for debugging purposes + console.log( "A new message was received: ", topic ); + + // Use the data that was passed from our subject + // to display a message preview to the user + $( ".messageSender" ).html( data.sender ); + $( ".messagePreview" ).html( data.body ); + +}); + +// Here's another subscriber using the same data to perform +// a different task. + +// Update the counter displaying the number of new +// messages received via the publisher + +var subscriber2 = subscribe( "inbox/newMessage", function( topic, data ) { + + $('.newMessageCounter').html( ++mailCounter ); + +}); + +publish( "inbox/newMessage", [{ + sender: "hello@google.com", + body: "Hey there! How are you doing today?" +}]); + +// We could then at a later point unsubscribe our subscribers +// from receiving any new topic notifications as follows: +// unsubscribe( subscriber1 ); +// unsubscribe( subscriber2 ); + + +//*******************************************************// +// Publish/Subscribe Implementations +//*******************************************************// + +//********************** Snippet 1 **********************// + +// Publish + +// jQuery: $(obj).trigger("channel", [arg1, arg2, arg3]); +$( el ).trigger( "/login", [{username:"test", userData:"test"}] ); + +// Dojo: dojo.publish("channel", [arg1, arg2, arg3] ); +dojo.publish( "/login", [{username:"test", userData:"test"}] ); + +// YUI: el.publish("channel", [arg1, arg2, arg3]); +el.publish( "/login", {username:"test", userData:"test"} ); + + +// Subscribe + +// jQuery: $(obj).on( "channel", [data], fn ); +$( el ).on( "/login", function( event ){...} ); + +// Dojo: dojo.subscribe( "channel", fn); +var handle = dojo.subscribe( "/login", function(data){..} ); + +// YUI: el.on("channel", handler); +el.on( "/login", function( data ){...} ); + + +// Unsubscribe + +// jQuery: $(obj).off( "channel" ); +$( el ).off( "/login" ); + +// Dojo: dojo.unsubscribe( handle ); +dojo.unsubscribe( handle ); + +// YUI: el.detach("channel"); +el.detach( "/login" ); + +//********************** Snippet 2 **********************// + +var pubsub = {}; + +(function(myObject) { + + // Storage for topics that can be broadcast + // or listened to + var topics = {}; + + // A topic identifier + var subUid = -1; + + // Publish or broadcast events of interest + // with a specific topic name and arguments + // such as the data to pass along + myObject.publish = function( topic, args ) { + + if ( !topics[topic] ) { + return false; + } + + var subscribers = topics[topic], + len = subscribers ? subscribers.length : 0; + + while (len--) { + subscribers[len].func( topic, args ); + } + + return this; + }; + + // Subscribe to events of interest + // with a specific topic name and a + // callback function, to be executed + // when the topic/event is observed + myObject.subscribe = function( topic, func ) { + + if (!topics[topic]) { + topics[topic] = []; + } + + var token = ( ++subUid ).toString(); + topics[topic].push({ + token: token, + func: func + }); + return token; + }; + + // Unsubscribe from a specific + // topic, based on a tokenized reference + // to the subscription + myObject.unsubscribe = function( token ) { + for ( var m in topics ) { + if ( topics[m] ) { + for ( var i = 0, j = topics[m].length; i < j; i++ ) { + if ( topics[m][i].token === token ) { + topics[m].splice( i, 1 ); + return token; + } + } + } + } + return this; + }; +}( pubsub )); + +//********************** Snippet 3 **********************// + +// Another simple message handler + +// A simple message logger that logs any topics and data received through our +// subscriber +var messageLogger = function ( topics, data ) { + console.log( "Logging: " + topics + ": " + data ); +}; + +// Subscribers listen for topics they have subscribed to and +// invoke a callback function (e.g messageLogger) once a new +// notification is broadcast on that topic +var subscription = pubsub.subscribe( "inbox/newMessage", messageLogger ); + +// Publishers are in charge of publishing topics or notifications of +// interest to the application. e.g: + +pubsub.publish( "inbox/newMessage", "hello world!" ); + +// or +pubsub.publish( "inbox/newMessage", ["test", "a", "b", "c"] ); + +// or +pubsub.publish( "inbox/newMessage", { + sender: "hello@google.com", + body: "Hey again!" +}); + +// We can also unsubscribe if we no longer wish for our subscribers +// to be notified +pubsub.unsubscribe( subscription ); + +// Once unsubscribed, this for example won't result in our +// messageLogger being executed as the subscriber is +// no longer listening +pubsub.publish( "inbox/newMessage", "Hello! are you still there?" ); + +//********************** Snippet 4 **********************// + +// Return the current local time to be used in our UI later +getCurrentTime = function (){ + + var date = new Date(), + m = date.getMonth() + 1, + d = date.getDate(), + y = date.getFullYear(), + t = date.toLocaleTimeString().toLowerCase(); + + return (m + "/" + d + "/" + y + " " + t); + }; + + // Add a new row of data to our fictional grid component + function addGridRow( data ) { + + // ui.grid.addRow( data ); + console.log( "updated grid component with:" + data ); + + } + + // Update our fictional grid to show the time it was last + // updated + function updateCounter( data ) { + + // ui.grid.updateLastChanged( getCurrentTime() ); + console.log( "data last updated at: " + getCurrentTime() + " with " + data); + + } + + // Update the grid using the data passed to our subscribers + gridUpdate = function( topic, data ){ + + if ( data !== undefined ) { + addGridRow( data ); + updateCounter( data ); + } + + }; + + // Create a subscription to the newDataAvailable topic + var subscriber = pubsub.subscribe( "newDataAvailable", gridUpdate ); + + // The following represents updates to our data layer. This could be + // powered by ajax requests which broadcast that new data is available + // to the rest of the application. + + // Publish changes to the gridUpdated topic representing new entries + pubsub.publish( "newDataAvailable", { + summary: "Apple made $5 billion", + identifier: "APPL", + stockPrice: 570.91 + }); + + pubsub.publish( "newDataAvailable", { + summary: "Microsoft made $20 million", + identifier: "MSFT", + stockPrice: 30.85 + }); + +//********************** Snippet 5 **********************// + + + + + + +
    + +
    +

    + + +

    +

    + + +

    +

    + + + +

    +

    + + +

    +
    + + + +
    +

    Recent users

    +

    Recent movies rated

    +
    + +
    + + +//********************** Snippet 6 **********************// + +;(function( $ ) { + + // Pre-compile templates and "cache" them using closure + var + userTemplate = _.template($( "#userTemplate" ).html()), + ratingsTemplate = _.template($( "#ratingsTemplate" ).html()); + + // Subscribe to the new user topic, which adds a user + // to a list of users who have submitted reviews + $.subscribe( "/new/user", function( e, data ){ + + if( data ){ + + $('#users').append( userTemplate( data )); + + } + + }); + + // Subscribe to the new rating topic. This is composed of a title and + // rating. New ratings are appended to a running list of added user + // ratings. + $.subscribe( "/new/rating", function( e, data ){ + + if( data ){ + + $( "#ratings" ).append( ratingsTemplate( data ) ); + + } + + }); + + // Handler for adding a new user + $("#add").on("click", function( e ) { + + e.preventDefault(); + + var strUser = $("#twitter_handle").val(), + strMovie = $("#movie_seen").val(), + strRating = $("#movie_rating").val(); + + // Inform the application a new user is available + $.publish( "/new/user", { name: strUser } ); + + // Inform the app a new rating is available + $.publish( "/new/rating", { title: strMovie, rating: strRating} ); + + }); + + })( jQuery ); + + + //********************** Snippet 7 **********************// + +
    + + + + + +
    + + + +
    + +
      + + + + + + //********************** Snippet 8 **********************// + + ;(function( $ ) { + + // Pre-compile template and "cache" it using closure + var resultTemplate = _.template($( "#resultTemplate" ).html()); + + // Subscribe to the new search tags topic + $.subscribe( "/search/tags", function( e, tags ) { + $( "#lastQuery" ) + .html("

      Searched for:" + tags + "

      "); + }); + + // Subscribe to the new results topic + $.subscribe( "/search/resultSet", function( e, results ){ + + $( "#searchResults" ).empty().append(resultTemplate( results )); + + }); + + // Submit a search query and publish tags on the /search/tags topic + $( "#flickrSearch" ).submit( function( e ) { + + e.preventDefault(); + var tags = $(this).find( "#query").val(); + + if ( !tags ){ + return; + } + + $.publish( "/search/tags", [ $.trim(tags) ]); + + }); + + + // Subscribe to new tags being published and perform + // a search query using them. Once data has returned + // publish this data for the rest of the application + // to consume + + $.subscribe("/search/tags", function( e, tags ) { + + $.getJSON( "http://api.flickr.com/services/feeds/photos_public.gne?jsoncallback=?", { + tags: tags, + tagmode: "any", + format: "json" + }, + + function( data ){ + + if( !data.items.length ) { + return; + } + + $.publish( "/search/resultSet", { items: data.items } ); + }); + + }); + + + })( jQuery ); \ No newline at end of file diff --git a/book/snippets/01-JavaScript-Design-Patterns/6-the-mediator-pattern.es2015.js b/book/snippets/01-JavaScript-Design-Patterns/6-the-mediator-pattern.es2015.js new file mode 100644 index 00000000..91bb9187 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/6-the-mediator-pattern.es2015.js @@ -0,0 +1,67 @@ +//********************** Snippet 1 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration + +const mediator = {}; + +//********************** Snippet 2 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax + +const orgChart = { + addNewEmployee() { + // getEmployeeDetail provides a view that users interact with + const employeeDetail = this.getEmployeeDetail(); + + // when the employee detail is complete, the mediator (the 'orgchart' object) + // decides what should happen next + // [ES2015+] Parentheses are optional when there is only one parameter + employeeDetail.on('complete', employee => { + // set up additional objects that have additional events, which are used + // by the mediator to do additional things + // [ES2015+] Parentheses are optional when there is only one parameter + const managerSelector = this.selectManager(employee); + managerSelector.on('save', employee => { + employee.save(); + }); + }); + }, + + // ... +}; + +//********************** Snippet 3 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance +// [ES2015+] We used new template literals for string interpolation + +const MenuItem = MyFrameworkView.extend({ + events: { + 'click .thatThing': 'clickedIt', + }, + + clickedIt(e) { + e.preventDefault(); + + // assume this triggers "menu:click:foo" + MyFramework.trigger(`menu:click:${this.model.get('name')}`); + }, +}); + +// ... somewhere else in the app + +class MyWorkflow { + constructor() { + MyFramework.on('menu:click:foo', this.doStuff, this); + } + + // [ES2015+] The static keyword defines a static method for a class. + // [ES2015+] Static methods are called without instantiating their class and cannot be called through a class instance. + // [ES2015+] Static methods are often used to create utility functions for an application. + static doStuff() { + // instantiate multiple objects here. + // set up event handlers for those objects. + // coordinate all of the objects into a meaningful workflow. + } +} diff --git a/book/snippets/01-JavaScript-Design-Patterns/6-the-mediator-pattern.es5.js b/book/snippets/01-JavaScript-Design-Patterns/6-the-mediator-pattern.es5.js new file mode 100644 index 00000000..4b7aa1b5 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/6-the-mediator-pattern.es5.js @@ -0,0 +1,60 @@ +//********************** Snippet 1 **********************// + +var mediator = {}; + +//********************** Snippet 2 **********************// + +var orgChart = { + + addNewEmployee: function(){ + + // getEmployeeDetail provides a view that users interact with + var employeeDetail = this.getEmployeeDetail(); + + // when the employee detail is complete, the mediator (the 'orgchart' object) + // decides what should happen next + employeeDetail.on("complete", function(employee){ + + // set up additional objects that have additional events, which are used + // by the mediator to do additional things + var managerSelector = this.selectManager(employee); + managerSelector.on("save", function(employee){ + employee.save(); + }); + + }); + }, + + // ... + } + +//********************** Snippet 3 **********************// + +var MenuItem = MyFrameworkView.extend({ + + events: { + "click .thatThing": "clickedIt" + }, + + clickedIt: function(e){ + e.preventDefault(); + + // assume this triggers "menu:click:foo" + MyFramework.trigger("menu:click:" + this.model.get("name")); + } + + }); + + // ... somewhere else in the app + + var MyWorkflow = function(){ + MyFramework.on("menu:click:foo", this.doStuff, this); + }; + + MyWorkflow.prototype.doStuff = function(){ + // instantiate multiple objects here. + // set up event handlers for those objects. + // coordinate all of the objects into a meaningful workflow. + }; + + \ No newline at end of file diff --git a/book/snippets/01-JavaScript-Design-Patterns/7-the-prototype-pattern.es2015.js b/book/snippets/01-JavaScript-Design-Patterns/7-the-prototype-pattern.es2015.js new file mode 100644 index 00000000..163e85d7 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/7-the-prototype-pattern.es2015.js @@ -0,0 +1,94 @@ +//********************** Snippet 1 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration + +const myCar = { + name: 'Ford Escort', + + drive() { + console.log("Weeee. I'm driving!"); + }, + + panic() { + console.log('Wait. How do you stop this thing?'); + }, +}; + +// Use Object.create to instantiate a new car +const yourCar = Object.create(myCar); + +// Now we can see that one is a prototype of the other +console.log(yourCar.name); + +//********************** Snippet 2 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new template literals for string interpolation + +const vehicle = { + getModel() { + console.log(`The model of this vehicle is..${this.model}`); + }, +}; + +const car = Object.create(vehicle, { + id: { + value: MY_GLOBAL.nextId(), + // writable:false, configurable:false by default + enumerable: true, + }, + + model: { + value: 'Ford', + enumerable: true, + }, +}); + +//********************** Snippet 3 **********************// +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method and method declaration +// [ES2015+] We have new pattern implementation with new inheritance +// [ES2015+] Classes are syntactic sugar over JavaScript's prototype-based inheritance +// [ES2015+] We used new keyword const for immutable constant declaration + +class VehiclePrototype { + constructor(model) { + this.model = model; + } + + getModel() { + console.log('The model of this vehicle is..' + this.model); + } + + Clone() {} +} +// [ES2015+] The extends keyword is used to create a class which is a child of another class. +// [ES2015+] A constructor can use the super keyword to call the constructor of the super class. +class Vehicle extends VehiclePrototype { + constructor(model) { + super(model); + } + Clone() { + return new Vehicle(this.model); + } +} + +const car = new Vehicle('Ford Escort'); +const car2 = car.Clone(); +car2.getModel(); + +//********************** Snippet 4 **********************// +// [ES2015+] We used new arrow function syntax +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] Below we used new class declaration, using keyword class +// [ES2015+] We used new constructor method +// [ES2015+] We still could use Object.prototype for adding new methods, because internally we use the same structure + +const beget = (() => { + class F { + constructor() {} + } + + return proto => { + F.prototype = proto; + return new F(); + }; +})(); diff --git a/book/snippets/01-JavaScript-Design-Patterns/7-the-prototype-pattern.es5.js b/book/snippets/01-JavaScript-Design-Patterns/7-the-prototype-pattern.es5.js new file mode 100644 index 00000000..98c25443 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/7-the-prototype-pattern.es5.js @@ -0,0 +1,85 @@ +//********************** Snippet 1 **********************// + +var myCar = { + + name: "Ford Escort", + + drive: function () { + console.log( "Weeee. I'm driving!" ); + }, + + panic: function () { + console.log( "Wait. How do you stop this thing?" ); + } + +}; + +// Use Object.create to instantiate a new car +var yourCar = Object.create( myCar ); + +// Now we can see that one is a prototype of the other +console.log( yourCar.name ); + +//********************** Snippet 2 **********************// + +var vehicle = { + getModel: function () { + console.log( "The model of this vehicle is.." + this.model ); + } +}; + +var car = Object.create(vehicle, { + + "id": { + value: MY_GLOBAL.nextId(), + // writable:false, configurable:false by default + enumerable: true + }, + + "model": { + value: "Ford", + enumerable: true + } + +}); + +//********************** Snippet 3 **********************// + +var vehiclePrototype = { + + init: function ( carModel ) { + this.model = carModel; + }, + + getModel: function () { + console.log( "The model of this vehicle is.." + this.model); + } +}; + + +function vehicle( model ) { + + function F() {}; + F.prototype = vehiclePrototype; + + var f = new F(); + + f.init( model ); + return f; + +} + +var car = vehicle( "Ford Escort" ); +car.getModel(); + +//********************** Snippet 4 **********************// + +var beget = (function () { + + function F() {} + + return function ( proto ) { + F.prototype = proto; + return new F(); + }; +})(); diff --git a/book/snippets/01-JavaScript-Design-Patterns/8-the-command-pattern.es2015.js b/book/snippets/01-JavaScript-Design-Patterns/8-the-command-pattern.es2015.js new file mode 100644 index 00000000..46a4244a --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/8-the-command-pattern.es2015.js @@ -0,0 +1,43 @@ +//********************** Snippet 1 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new template literals for string interpolation +// [ES2015+] We used new arrow function syntax + +(() => { + const carManager = { + // request information + requestInfo(model, id) { + return `The information for ${model} with ID ${id} is foobar`; + }, + + // purchase the car + buyVehicle(model, id) { + return `You have successfully purchased Item ${id}, a ${model}`; + }, + + // arrange a viewing + arrangeViewing(model, id) { + return `You have successfully booked a viewing of ${model} ( ${id} ) `; + }, + }; +})(); + +//********************** Snippet 2 **********************// + +carManager.execute('buyVehicle', 'Ford Escort', '453543'); + +//********************** Snippet 3 **********************// + +carManager.execute = function(name) { + return ( + carManager[name] && + carManager[name].apply(carManager, [].slice.call(arguments, 1)) + ); +}; + +//********************** Snippet 4 **********************// + +carManager.execute('arrangeViewing', 'Ferrari', '14523'); +carManager.execute('requestInfo', 'Ford Mondeo', '54323'); +carManager.execute('requestInfo', 'Ford Escort', '34232'); +carManager.execute('buyVehicle', 'Ford Escort', '34232'); diff --git a/book/snippets/01-JavaScript-Design-Patterns/8-the-command-pattern.es5.js b/book/snippets/01-JavaScript-Design-Patterns/8-the-command-pattern.es5.js new file mode 100644 index 00000000..c6726361 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/8-the-command-pattern.es5.js @@ -0,0 +1,41 @@ +//********************** Snippet 1 **********************// + +(function(){ + + var carManager = { + + // request information + requestInfo: function( model, id ){ + return "The information for " + model + " with ID " + id + " is foobar"; + }, + + // purchase the car + buyVehicle: function( model, id ){ + return "You have successfully purchased Item " + id + ", a " + model; + }, + + // arrange a viewing + arrangeViewing: function( model, id ){ + return "You have successfully booked a viewing of " + model + " ( " + id + " ) "; + } + + }; + +})(); + +//********************** Snippet 2 **********************// + +carManager.execute( "buyVehicle", "Ford Escort", "453543" ); + +//********************** Snippet 3 **********************// + +carManager.execute = function ( name ) { + return carManager[name] && carManager[name].apply( carManager, [].slice.call(arguments, 1) ); +}; + +//********************** Snippet 4 **********************// + +carManager.execute( "arrangeViewing", "Ferrari", "14523" ); +carManager.execute( "requestInfo", "Ford Mondeo", "54323" ); +carManager.execute( "requestInfo", "Ford Escort", "34232" ); +carManager.execute( "buyVehicle", "Ford Escort", "34232" ); \ No newline at end of file diff --git a/book/snippets/01-JavaScript-Design-Patterns/9-the-facade-pattern.es2015.js b/book/snippets/01-JavaScript-Design-Patterns/9-the-facade-pattern.es2015.js new file mode 100644 index 00000000..74036f3f --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/9-the-facade-pattern.es2015.js @@ -0,0 +1,75 @@ +//********************** Snippet 1 **********************// +// [ES2015+] We used new template literals for string interpolation +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax + +const addMyEvent = (el, ev, fn) => { + if (el.addEventListener) { + el.addEventListener(ev, fn, false); + } else if (el.attachEvent) { + el.attachEvent(`on${ev}`, fn); + } else { + el[`on${ev}`] = fn; + } + }; + +//********************** Snippet 2 **********************// + +bindReady() { + ... + if (document.addEventListener) { + // Use the handy event callback + document.addEventListener('DOMContentLoaded', DOMContentLoaded, false); + + // A fallback to window.onload, that will always work + window.addEventListener('load', jQuery.ready, false); + + // If IE event model is used + } else if (document.attachEvent) { + + document.attachEvent('onreadystatechange', DOMContentLoaded); + + // A fallback to window.onload, that will always work + window.attachEvent('onload', jQuery.ready); + ... + + +//********************** Snippet 3 **********************// +// [ES2015+] We have new pattern implementation with new keywords import and export + +const _private = { + i: 5, + get() { + console.log(`current value:${this.i}`); + }, + set(val) { + this.i = val; + }, + run() { + console.log('running'); + }, + jump() { + console.log('jumping'); + }, + }; + + // [ES2015+] We used the destructuring assignment syntax that makes it possible to unpack values from data structures into distinct variables. + const module = { + facade({ val, run }) { + _private.set(val); + _private.get(); + if (run) { + _private.run(); + } + }, + }; + // [ES2015+] Default export module, without name + export default module; + + // [ES2015+] The import statement is used to import bindings which are exported by another module. + import module from './module'; + // Outputs: "current value: 10" and "running" + module.facade({ + run: true, + val: 10, + }); diff --git a/book/snippets/01-JavaScript-Design-Patterns/9-the-facade-pattern.es5.js b/book/snippets/01-JavaScript-Design-Patterns/9-the-facade-pattern.es5.js new file mode 100644 index 00000000..caa196d2 --- /dev/null +++ b/book/snippets/01-JavaScript-Design-Patterns/9-the-facade-pattern.es5.js @@ -0,0 +1,70 @@ +//********************** Snippet 1 **********************// + +var addMyEvent = function( el,ev,fn ){ + + if( el.addEventListener ){ + el.addEventListener( ev,fn, false ); + }else if(el.attachEvent){ + el.attachEvent( "on" + ev, fn ); + } else{ + el["on" + ev] = fn; + } + +}; + + +//********************** Snippet 2 **********************// +bindReady: function() { + ... + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + ... + + + +//********************** Snippet 3 **********************// +var module = (function() { + + var _private = { + i: 5, + get: function() { + console.log( "current value:" + this.i); + }, + set: function( val ) { + this.i = val; + }, + run: function() { + console.log( "running" ); + }, + jump: function(){ + console.log( "jumping" ); + } + }; + + return { + + facade: function( args ) { + _private.set(args.val); + _private.get(); + if ( args.run ) { + _private.run(); + } + } + }; +}()); + + +// Outputs: "current value: 10" and "running" +module.facade( {run: true, val: 10} ); \ No newline at end of file diff --git a/book/snippets/02-JavaScript-MV*-Patterns/14-MVC.es2015.js b/book/snippets/02-JavaScript-MV*-Patterns/14-MVC.es2015.js new file mode 100644 index 00000000..7eb0a832 --- /dev/null +++ b/book/snippets/02-JavaScript-MV*-Patterns/14-MVC.es2015.js @@ -0,0 +1,137 @@ +//*******************************************************// +// Models +//*******************************************************// +// [ES2015+] We used new keyword const for immutable constant declaration + +//********************** Snippet 1 **********************// +const Photo = Backbone.Model.extend({ + // Default attributes for the photo + defaults: { + src: 'placeholder.jpg', + caption: 'A default image', + viewed: false, + }, + + // Ensure that each photo created has an `src`. + initialize: function() { + this.set({ src: this.defaults.src }); + }, +}); + +//********************** Snippet 2 **********************// +const PhotoGallery = Backbone.Collection.extend({ + // Reference to this collection's model. + model: Photo, + + // Filter down the list of all photos + // that have been viewed + viewed: function() { + return this.filter(function(photo) { + return photo.get('viewed'); + }); + }, + + // Filter down the list to only photos that + // have not yet been viewed + unviewed: function() { + return this.without.apply(this, this.viewed()); + }, +}); + +//*******************************************************// +// Models +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] We used new arrow function syntax + +const buildPhotoView = (photoModel, photoController) => { + const base = document.createElement('div'); + const photoEl = document.createElement('div'); + + base.appendChild(photoEl); + + const render = () => { + // We use a templating library such as Underscore + // templating which generates the HTML for our + // photo entry + photoEl.innerHTML = _.template('#photoTemplate', { + src: photoModel.getSrc(), + }); + }; + + photoModel.addSubscriber(render); + + photoEl.addEventListener('click', () => { + photoController.handleEvent('click', photoModel); + }); + + const show = () => { + photoEl.style.display = ''; + }; + + const hide = () => { + photoEl.style.display = 'none'; + }; + + return { + showView: show, + hideView: hide, + }; +}; + + +//********************** Snippet 2 **********************// +
    1. +

      {{caption}}

      + +
      + {{metadata}} +
      +
    2. + +//********************** Snippet 3 **********************// +
    3. +

      <%= caption %>

      + +
      + <%= metadata %> +
      +
    4. + +//*******************************************************// +// Controllers +//*******************************************************// + +//********************** Snippet 1 **********************// +// Controllers in Spine are created by inheriting from Spine.Controller + +const PhotosController = Spine.Controller.sub({ + init: function() { + this.item.bind('update', this.proxy(this.render)); + this.item.bind('destroy', this.proxy(this.remove)); + }, + + render: function() { + // Handle templating + this.replace($('#photoTemplate').tmpl(this.item)); + return this; + }, + + remove: function() { + this.el.remove(); + this.release(); + }, +}); + +//********************** Snippet 2 **********************// +const PhotoRouter = Backbone.Router.extend({ + routes: { 'photos/:id': 'route' }, + + route: function(id) { + const item = photoCollection.get(id); + const view = new PhotoView({ model: item }); + + $('.content').html(view.render().el); + }, +}); diff --git a/book/snippets/02-JavaScript-MV*-Patterns/14-MVC.es5.js b/book/snippets/02-JavaScript-MV*-Patterns/14-MVC.es5.js new file mode 100644 index 00000000..832170c4 --- /dev/null +++ b/book/snippets/02-JavaScript-MV*-Patterns/14-MVC.es5.js @@ -0,0 +1,139 @@ +//*******************************************************// +// Models +//*******************************************************// + +//********************** Snippet 1 **********************// +var Photo = Backbone.Model.extend({ + + // Default attributes for the photo + defaults: { + src: "placeholder.jpg", + caption: "A default image", + viewed: false + }, + + // Ensure that each photo created has an `src`. + initialize: function() { + this.set( { "src": this.defaults.src} ); + } + +}); + +//********************** Snippet 2 **********************// +var PhotoGallery = Backbone.Collection.extend({ + + // Reference to this collection's model. + model: Photo, + + // Filter down the list of all photos + // that have been viewed + viewed: function() { + return this.filter(function( photo ){ + return photo.get( "viewed" ); + }); + }, + + // Filter down the list to only photos that + // have not yet been viewed + unviewed: function() { + return this.without.apply( this, this.viewed() ); + } +}); + +//*******************************************************// +// Models +//*******************************************************// + +//********************** Snippet 1 **********************// +var buildPhotoView = function ( photoModel, photoController ) { + + var base = document.createElement( "div" ), + photoEl = document.createElement( "div" ); + + base.appendChild(photoEl); + + var render = function () { + // We use a templating library such as Underscore + // templating which generates the HTML for our + // photo entry + photoEl.innerHTML = _.template( "#photoTemplate", { + src: photoModel.getSrc() + }); + }; + + photoModel.addSubscriber( render ); + + photoEl.addEventListener( "click", function () { + photoController.handleEvent( "click", photoModel ); + }); + + var show = function () { + photoEl.style.display = ""; + }; + + var hide = function () { + photoEl.style.display = "none"; + }; + + return { + showView: show, + hideView: hide + }; + +}; + +//********************** Snippet 2 **********************// +
    5. +

      {{caption}}

      + +
      + {{metadata}} +
      +
    6. + +//********************** Snippet 3 **********************// +
    7. +

      <%= caption %>

      + +
      + <%= metadata %> +
      +
    8. + +//*******************************************************// +// Controllers +//*******************************************************// + +//********************** Snippet 1 **********************// +// Controllers in Spine are created by inheriting from Spine.Controller + +var PhotosController = Spine.Controller.sub({ + + init: function () { + this.item.bind( "update", this.proxy( this.render )); + this.item.bind( "destroy", this.proxy( this.remove )); + }, + + render: function () { + // Handle templating + this.replace( $( "#photoTemplate" ).tmpl( this.item ) ); + return this; + }, + + remove: function () { + this.el.remove(); + this.release(); + } +}); + +//********************** Snippet 2 **********************// +var PhotoRouter = Backbone.Router.extend({ + routes: { "photos/:id": "route" }, + + route: function( id ) { + var item = photoCollection.get( id ); + var view = new PhotoView( { model: item } ); + + $('.content').html( view.render().el ); + } +}); diff --git a/book/snippets/02-JavaScript-MV*-Patterns/15-MVP.es5-no-changes.js b/book/snippets/02-JavaScript-MV*-Patterns/15-MVP.es5-no-changes.js new file mode 100644 index 00000000..e5fde046 --- /dev/null +++ b/book/snippets/02-JavaScript-MV*-Patterns/15-MVP.es5-no-changes.js @@ -0,0 +1,42 @@ +//*******************************************************// +// MVC, MVP and Backbone.js +//*******************************************************// + +//********************** Snippet 1 **********************// +var PhotoView = Backbone.View.extend({ + + //... is a list tag. + tagName: "li", + + // Pass the contents of the photo template through a templating + // function, cache it for a single photo + template: _.template( $("#photo-template").html() ), + + // The DOM events specific to an item. + events: { + "click img": "toggleViewed" + }, + + // The PhotoView listens for changes to + // its model, re-rendering. Since there's + // a one-to-one correspondence between a + // **Photo** and a **PhotoView** in this + // app, we set a direct reference on the model for convenience. + + initialize: function() { + this.model.on( "change", this.render, this ); + this.model.on( "destroy", this.remove, this ); + }, + + // Re-render the photo entry + render: function() { + $( this.el ).html( this.template(this.model.toJSON() )); + return this; + }, + + // Toggle the `"viewed"` state of the model. + toggleViewed: function() { + this.model.viewed(); + } + +}); diff --git a/book/snippets/02-JavaScript-MV*-Patterns/16-MVVM.es5-no-changes.js b/book/snippets/02-JavaScript-MV*-Patterns/16-MVVM.es5-no-changes.js new file mode 100644 index 00000000..f07d2e29 --- /dev/null +++ b/book/snippets/02-JavaScript-MV*-Patterns/16-MVVM.es5-no-changes.js @@ -0,0 +1,129 @@ +//*******************************************************// +// Model +//*******************************************************// + +//********************** Snippet 1 **********************// +var Todo = function ( content, done ) { + this.content = ko.observable(content); + this.done = ko.observable(done); + this.editing = ko.observable(false); +}; + +//*******************************************************// +// View +//*******************************************************// + +//********************** Snippet 1 **********************// +var aViewModel = { + contactName: ko.observable("John") +}; +ko.applyBindings(aViewModel); + +//********************** Snippet 2 **********************// +

      +
      + You have a really long name! +
      +

      Contact name:

      + +//********************** Snippet 3 **********************// +
      +
      +

      Todos

      + +
      +
      + + + + + + +
      +
      + + + +//*******************************************************// +// ViewModel +//*******************************************************// + +//********************** Snippet 1 **********************// +// our main ViewModel + var ViewModel = function ( todos ) { + var self = this; + + // map array of passed in todos to an observableArray of Todo objects + self.todos = ko.observableArray( + ko.utils.arrayMap( todos, function ( todo ) { + return new Todo( todo.content, todo.done ); + })); + + // store the new todo value being entered + self.current = ko.observable(); + + // add a new todo, when enter key is pressed + self.add = function ( data, event ) { + var newTodo, current = self.current().trim(); + if ( current ) { + newTodo = new Todo( current ); + self.todos.push( newTodo ); + self.current(""); + } + }; + + // remove a single todo + self.remove = function ( todo ) { + self.todos.remove( todo ); + }; + + // remove all completed todos + self.removeCompleted = function () { + self.todos.remove(function (todo) { + return todo.done(); + }); + }; + + // writeable computed observable to handle marking all complete/incomplete + self.allCompleted = ko.computed({ + + // always return true/false based on the done flag of all todos + read:function () { + return !self.remainingCount(); + }, + + // set all todos to the written value (true/false) + write:function ( newValue ) { + ko.utils.arrayForEach( self.todos(), function ( todo ) { + //set even if value is the same, as subscribers are not notified in that case + todo.done( newValue ); + }); + } + }); + + // edit an item + self.editItem = function( item ) { + item.editing( true ); + }; +.. + +//********************** Snippet 2 **********************// +// Define an initially an empty array +var myObservableArray = ko.observableArray(); + +// Add a value to the array and notify our observers +myObservableArray.push( 'A new todo item' ); diff --git a/book/snippets/02-JavaScript-MV*-Patterns/17-MVVM-with-looser-data-bindings.es5-no-changes.js b/book/snippets/02-JavaScript-MV*-Patterns/17-MVVM-with-looser-data-bindings.es5-no-changes.js new file mode 100644 index 00000000..75e466d8 --- /dev/null +++ b/book/snippets/02-JavaScript-MV*-Patterns/17-MVVM-with-looser-data-bindings.es5-no-changes.js @@ -0,0 +1,192 @@ +//********************** Snippet 1 **********************// + + +//********************** Snippet 2 **********************// +var ourBindingProvider = { + nodeHasBindings: function( node ) { + // returns true/false + }, + + getBindings: function( node, bindingContext ) { + // returns a binding object + } +}; + +//********************** Snippet 3 **********************// +// does an element have any bindings? +function nodeHasBindings( node ) { + return node.getAttribute ? node.getAttribute("data-class") : false; +}; + +//********************** Snippet 4 **********************// +var viewModel = new ViewModel( todos || [] ), + bindings = { + + newTodo: { + value: viewModel.current, + valueUpdate: "afterkeydown", + enterKey: viewModel.add + }, + + taskTooltip: { + visible: viewModel.showTooltip + }, + checkAllContainer: { + visible: viewModel.todos().length + }, + checkAll: { + checked: viewModel.allCompleted + }, + + todos: { + foreach: viewModel.todos + }, + todoListItem: function() { + return { + css: { + editing: this.editing + } + }; + }, + todoListItemWrapper: function() { + return { + css: { + done: this.done + } + }; + }, + todoCheckBox: function() { + return { + checked: this.done + }; + }, + todoContent: function() { + return { + text: this.content, + event: { + dblclick: this.edit + } + }; + }, + todoDestroy: function() { + return { + click: viewModel.remove + }; + }, + + todoEdit: function() { + return { + value: this.content, + valueUpdate: "afterkeydown", + enterKey: this.stopEditing, + event: { + blur: this.stopEditing + } + }; + }, + + todoCount: { + visible: viewModel.remainingCount + }, + remainingCount: { + text: viewModel.remainingCount + }, + remainingCountWord: function() { + return { + text: viewModel.getLabel(viewModel.remainingCount) + }; + }, + todoClear: { + visible: viewModel.completedCount + }, + todoClearAll: { + click: viewModel.removeCompleted + }, + completedCount: { + text: viewModel.completedCount + }, + completedCountWord: function() { + return { + text: viewModel.getLabel(viewModel.completedCount) + }; + }, + todoInstructions: { + visible: viewModel.todos().length + } + }; + + .... + +//********************** Snippet 5 **********************// + // We can now create a bindingProvider that uses + // something different than data-bind attributes + ko.customBindingProvider = function( bindingObject ) { + this.bindingObject = bindingObject; + + // determine if an element has any bindings + this.nodeHasBindings = function( node ) { + return node.getAttribute ? node.getAttribute( "data-class" ) : false; + }; + }; + + // return the bindings given a node and the bindingContext + this.getBindings = function( node, bindingContext ) { + + var result = {}, + classes = node.getAttribute( "data-class" ); + + if ( classes ) { + classes = classes.split( "" ); + + //evaluate each class, build a single object to return + for ( var i = 0, j = classes.length; i < j; i++ ) { + + var bindingAccessor = this.bindingObject[classes[i]]; + if ( bindingAccessor ) { + var binding = typeof bindingAccessor === "function" ? bindingAccessor.call(bindingContext.$data) : bindingAccessor; + ko.utils.extend(result, binding); + } + + } + } + + return result; + }; +}; + + +//********************** Snippet 6 **********************// +// set ko's current bindingProvider equal to our new binding provider +ko.bindingProvider.instance = new ko.customBindingProvider( bindings ); + +// bind a new instance of our ViewModel to the page +ko.applyBindings( viewModel ); + +})(); + +//********************** Snippet 7 **********************// +
      + + +
      +
      +
      + + +
      +
      +
    9. + +//********************** Snippet 4 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax +// [ES2015+] We used new object method declaration + +/* + * "Highly configurable" mutable plugin boilerplate + * Author: @markdalgleish + * Further changes, comments: @addyosmani + * Licensed under the MIT license + */ + +// Note that with this pattern, as per Alex Sexton's, the plugin logic +// hasn't been nested in a jQuery plugin. Instead, we just use +// jQuery for its instantiation. + +;(($, window, document, undefined) => { + // our plugin constructor + const Plugin = function(elem, options) { + this.elem = elem; + this.$elem = $(elem); + this.options = options; + + // This next line takes advantage of HTML5 data attributes + // to support customization of the plugin on a per-element + // basis. For example, + //
      + this.metadata = this.$elem.data('plugin-options'); + }; + + // the plugin prototype + Plugin.prototype = { + defaults: { + message: 'Hello world!', + }, + + init() { + // Introduce defaults that can be extended either + // globally or using an object literal. + this.config = $.extend({}, this.defaults, this.options, this.metadata); + + // Sample usage: + // Set the message per instance: + // $( "#elem" ).plugin( { message: "Goodbye World!"} ); + // or + // var p = new Plugin( document.getElementById( "elem" ), + // { message: "Goodbye World!"}).init() + // or, set the global default message: + // Plugin.defaults.message = "Goodbye World!" + + this.sampleMethod(); + return this; + }, + + sampleMethod() { + // e.g. show the currently configured message + // console.log(this.config.message); + }, + }; + + Plugin.defaults = Plugin.prototype.defaults; + + $.fn.plugin = function(options) { + return this.each(function() { + new Plugin(this, options).init(); + }); + }; + + // optional: window.Plugin = Plugin; + })(jQuery, window, document); + + //********************** Snippet 5 **********************// + $('#elem').plugin({ + message: 'foobar', + }); + diff --git a/book/snippets/04-Design-Patterns-In-jQuery/30-jQuery-plugin-design-patterns.es5.js b/book/snippets/04-Design-Patterns-In-jQuery/30-jQuery-plugin-design-patterns.es5.js new file mode 100644 index 00000000..6a9ca300 --- /dev/null +++ b/book/snippets/04-Design-Patterns-In-jQuery/30-jQuery-plugin-design-patterns.es5.js @@ -0,0 +1,902 @@ +//*******************************************************// +// Patterns +//*******************************************************// + +//********************** Snippet 1 **********************// +$.fn.myPluginName = function () { + // our plugin logic +}; + +//********************** Snippet 2 **********************// +(function( $ ){ + $.fn.myPluginName = function () { + // our plugin logic + }; +})( jQuery ); + +//********************** Snippet 3 **********************// +(function( $ ){ + $.extend($.fn, { + myplugin: function(){ + // your plugin logic + } + }); +})( jQuery ); + + +//*******************************************************// +// 'A Lightweight Start' Pattern +//*******************************************************// + +//********************** Snippet 1 **********************// +/*! + * jQuery lightweight plugin boilerplate + * Original author: @ajpiano + * Further changes, comments: @addyosmani + * Licensed under the MIT license + */ + + +// the semi-colon before the function invocation is a safety +// net against concatenated scripts and/or other plugins +// that are not closed properly. +;(function ( $, window, document, undefined ) { + + // undefined is used here as the undefined global + // variable in ECMAScript 3 and is mutable (i.e. it can + // be changed by someone else). undefined isn't really + // being passed in so we can ensure that its value is + // truly undefined. In ES5, undefined can no longer be + // modified. + + // window and document are passed through as local + // variables rather than as globals, because this (slightly) + // quickens the resolution process and can be more + // efficiently minified (especially when both are + // regularly referenced in our plugin). + + // Create the defaults once + var pluginName = "defaultPluginName", + defaults = { + propertyName: "value" + }; + + // The actual plugin constructor + function Plugin( element, options ) { + this.element = element; + + // jQuery has an extend method that merges the + // contents of two or more objects, storing the + // result in the first object. The first object + // is generally empty because we don't want to alter + // the default options for future instances of the plugin + this.options = $.extend( {}, defaults, options) ; + + this._defaults = defaults; + this._name = pluginName; + + this.init(); + } + + Plugin.prototype.init = function () { + // Place initialization logic here + // We already have access to the DOM element and + // the options via the instance, e.g. this.element + // and this.options + }; + + // A really lightweight plugin wrapper around the constructor, + // preventing against multiple instantiations + $.fn[pluginName] = function ( options ) { + return this.each(function () { + if ( !$.data(this, "plugin_" + pluginName )) { + $.data( this, "plugin_" + pluginName, + new Plugin( this, options )); + } + }); + } + +})( jQuery, window, document ); + +//********************** Snippet 2 **********************// +$("#elem").defaultPluginName({ + propertyName: "a custom value" +}); + + +//*******************************************************// +// “Complete” Widget Factory Pattern +//*******************************************************// + +//********************** Snippet 1 **********************// +/*! + * jQuery UI Widget-factory plugin boilerplate (for 1.8/9+) + * Author: @addyosmani + * Further changes: @peolanha + * Licensed under the MIT license + */ + +;(function ( $, window, document, undefined ) { + + // define our widget under a namespace of your choice + // with additional parameters e.g. + // $.widget( "namespace.widgetname", (optional) - an + // existing widget prototype to inherit from, an object + // literal to become the widget's prototype ); + + $.widget( "namespace.widgetname", { + + //Options to be used as defaults + options: { + someValue: null + }, + + //Setup widget (e.g. element creation, apply theming + //, bind events etc.) + _create: function () { + + // _create will automatically run the first time + // this widget is called. Put the initial widget + // setup code here, then we can access the element + // on which the widget was called via this.element. + // The options defined above can be accessed + // via this.options this.element.addStuff(); + }, + + // Destroy an instantiated plugin and clean up + // modifications the widget has made to the DOM + destroy: function () { + + // this.element.removeStuff(); + // For UI 1.8, destroy must be invoked from the + // base widget + $.Widget.prototype.destroy.call( this ); + // For UI 1.9, define _destroy instead and don't + // worry about + // calling the base widget + }, + + methodB: function ( event ) { + //_trigger dispatches callbacks the plugin user + // can subscribe to + // signature: _trigger( "callbackName", [eventObject], + // [uiObject] ) + // e.g. this._trigger( "hover", e /*where e.type == + // "mouseenter"*/, { hovered: $(e.target)}); + this._trigger( "methodA", event, { + key: value + }); + }, + + methodA: function ( event ) { + this._trigger( "dataChanged", event, { + key: value + }); + }, + + // Respond to any changes the user makes to the + // option method + _setOption: function ( key, value ) { + switch ( key ) { + case "someValue": + // this.options.someValue = doSomethingWith( value ); + break; + default: + // this.options[ key ] = value; + break; + } + + // For UI 1.8, _setOption must be manually invoked + // from the base widget + $.Widget.prototype._setOption.apply( this, arguments ); + // For UI 1.9 the _super method can be used instead + // this._super( "_setOption", key, value ); + } + }); + +})( jQuery, window, document ); + +//********************** Snippet 2 **********************// +var collection = $("#elem").widgetName({ + foo: false +}); + +collection.widgetName("methodB"); + + +//*******************************************************// +// Nested Namespacing Plugin Pattern +//*******************************************************// + +//********************** Snippet 1 **********************// +/*! + * jQuery namespaced "Starter" plugin boilerplate + * Author: @dougneiner + * Further changes: @addyosmani + * Licensed under the MIT license + */ + +;(function ( $ ) { + if (!$.myNamespace) { + $.myNamespace = {}; + }; + + $.myNamespace.myPluginName = function ( el, myFunctionParam, options ) { + // To avoid scope issues, use "base" instead of "this" + // to reference this class from internal events and functions. + var base = this; + + // Access to jQuery and DOM versions of element + base.$el = $( el ); + base.el = el; + + // Add a reverse reference to the DOM object + base.$el.data( "myNamespace.myPluginName", base ); + + base.init = function () { + base.myFunctionParam = myFunctionParam; + + base.options = $.extend({}, + $.myNamespace.myPluginName.defaultOptions, options); + + // Put our initialization code here + }; + + // Sample Function, Uncomment to use + // base.functionName = function( parameters ){ + // + // }; + // Run initializer + base.init(); + }; + + $.myNamespace.myPluginName.defaultOptions = { + myDefaultValue: "" + }; + + $.fn.mynamespace_myPluginName = function + ( myFunctionParam, options ) { + return this.each(function () { + (new $.myNamespace.myPluginName( this, + myFunctionParam, options )); + }); + }; + +})( jQuery ); + +//********************** Snippet 2 **********************// +$("#elem").mynamespace_myPluginName({ + myDefaultValue: "foobar" +}); + + +//*******************************************************// +// Custom Events Plugin Pattern (With The Widget factory) +//*******************************************************// + +//********************** Snippet 1 **********************// +/*! + * jQuery custom-events plugin boilerplate + * Author: DevPatch + * Further changes: @addyosmani + * Licensed under the MIT license + */ + +// In this pattern, we use jQuery's custom events to add +// pub/sub (publish/subscribe) capabilities to widgets. +// Each widget would publish certain events and subscribe +// to others. This approach effectively helps to decouple +// the widgets and enables them to function independently. + +;(function ( $, window, document, undefined ) { + $.widget( "ao.eventStatus", { + options: { + + }, + + _create: function() { + var self = this; + + //self.element.addClass( "my-widget" ); + + //subscribe to "myEventStart" + self.element.on( "myEventStart", function( e ) { + console.log( "event start" ); + }); + + //subscribe to "myEventEnd" + self.element.on( "myEventEnd", function( e ) { + console.log( "event end" ); + }); + + //unsubscribe to "myEventStart" + //self.element.off( "myEventStart", function(e){ + ///console.log( "unsubscribed to this event" ); + //}); + }, + + destroy: function(){ + $.Widget.prototype.destroy.apply( this, arguments ); + }, + }); +})( jQuery, window, document ); + +// Publishing event notifications +// $( ".my-widget" ).trigger( "myEventStart"); +// $( ".my-widget" ).trigger( "myEventEnd" ); + +//********************** Snippet 2 **********************// +var el = $( "#elem" ); +el.eventStatus(); +el.eventStatus().trigger( "myEventStart" ); + + +//*******************************************************// +// Prototypal Inheritance With The DOM-To-Object Bridge Pattern +//*******************************************************// + +//********************** Snippet 1 **********************// +/*! + * jQuery prototypal inheritance plugin boilerplate + * Author: Alex Sexton, Scott Gonzalez + * Further changes: @addyosmani + * Licensed under the MIT license + */ + + +// myObject - an object representing a concept we wish to model +// (e.g. a car) +var myObject = { + init: function( options, elem ) { + // Mix in the passed-in options with the default options + this.options = $.extend( {}, this.options, options ); + + // Save the element reference, both as a jQuery + // reference and a normal reference + this.elem = elem; + this.$elem = $( elem ); + + // Build the DOM's initial structure + this._build(); + + // return this so that we can chain and use the bridge with less code. + return this; + }, + options: { + name: "No name" + }, + _build: function(){ + //this.$elem.html( "

      "+this.options.name+"

      " ); + }, + myMethod: function( msg ){ + // We have direct access to the associated and cached + // jQuery element + // this.$elem.append( "

      "+msg+"

      " ); + } +}; + + +// Object.create support test, and fallback for browsers without it +if ( typeof Object.create !== "function" ) { + Object.create = function (o) { + function F() {} + F.prototype = o; + return new F(); + }; +} + + +// Create a plugin based on a defined object +$.plugin = function( name, object ) { + $.fn[name] = function( options ) { + return this.each(function() { + if ( ! $.data( this, name ) ) { + $.data( this, name, Object.create( object ).init( + options, this ) ); + } + }); + }; +}; + +//********************** Snippet 2 **********************// +$.plugin( "myobj", myObject ); + +$("#elem").myobj( {name: "John"} ); + +var collection = $( "#elem" ).data( "myobj" ); +collection.myMethod( "I am a method"); + + +//*******************************************************// +// jQuery UI Widget Factory Bridge Pattern +//*******************************************************// + +//********************** Snippet 1 **********************// +/*! + * jQuery UI Widget factory "bridge" plugin boilerplate + * Author: @erichynds + * Further changes, additional comments: @addyosmani + * Licensed under the MIT license + */ + + +// a "widgetName" object constructor +// required: this must accept two arguments, +// options: an object of configuration options +// element: the DOM element the instance was created on +var widgetName = function( options, element ){ + this.name = "myWidgetName"; + this.options = options; + this.element = element; + this._init(); +} + +// the "widgetName" prototype +widgetName.prototype = { + + // _create will automatically run the first time this + // widget is called + _create: function(){ + // creation code + }, + + // required: initialization logic for the plugin goes into _init + // This fires when our instance is first created and when + // attempting to initialize the widget again (by the bridge) + // after it has already been initialized. + _init: function(){ + // init code + }, + + // required: objects to be used with the bridge must contain an + // "option". Post-initialization, the logic for changing options + // goes here. + option: function( key, value ){ + + // optional: get/change options post initialization + // ignore if you don't require them. + + // signature: $("#foo").bar({ cool:false }); + if( $.isPlainObject( key ) ){ + this.options = $.extend( true, this.options, key ); + + // signature: $( "#foo" ).option( "cool" ); - getter + } else if ( key && typeof value === "undefined" ){ + return this.options[ key ]; + + // signature: $( "#foo" ).bar("option", "baz", false ); + } else { + this.options[ key ] = value; + } + + // required: option must return the current instance. + // When re-initializing an instance on elements, option + // is called first and is then chained to the _init method. + return this; + }, + + // notice no underscore is used for public methods + publicFunction: function(){ + console.log( "public function" ); + }, + + // underscores are used for private methods + _privateFunction: function(){ + console.log( "private function" ); + } +}; + +//********************** Snippet 2 **********************// +// connect the widget obj to jQuery's API under the "foo" namespace +$.widget.bridge( "foo", widgetName ); + +// create an instance of the widget for use +var instance = $( "#foo" ).foo({ + baz: true +}); + +// our widget instance exists in the elem's data +// Outputs: #elem +console.log(instance.data( "foo" ).element); + +// bridge allows us to call public methods +// Outputs: "public method" +instance.foo("publicFunction"); + +// bridge prevents calls to internal methods +instance.foo("_privateFunction"); + + +//*******************************************************// +// jQuery Mobile Widgets With The Widget factory +//*******************************************************// + +//********************** Snippet 1 **********************// +/*! + * (jQuery mobile) jQuery UI Widget-factory plugin boilerplate (for 1.8/9+) + * Author: @scottjehl + * Further changes: @addyosmani + * Licensed under the MIT license + */ + +;(function ( $, window, document, undefined ) { + + // define a widget under a namespace of our choice + // here "mobile" has been used in the first argument + $.widget( "mobile.widgetName", $.mobile.widget, { + + // Options to be used as defaults + options: { + foo: true, + bar: false + }, + + _create: function() { + // _create will automatically run the first time this + // widget is called. Put the initial widget set-up code + // here, then we can access the element on which + // the widget was called via this.element + // The options defined above can be accessed via + // this.options + + // var m = this.element, + // p = m.parents( ":jqmData(role="page")" ), + // c = p.find( ":jqmData(role="content")" ) + }, + + // Private methods/props start with underscores + _dosomething: function(){ ... }, + + // Public methods like these below can can be called + // externally: + // $("#myelem").foo( "enable", arguments ); + + enable: function() { ... }, + + // Destroy an instantiated plugin and clean up modifications + // the widget has made to the DOM + destroy: function () { + // this.element.removeStuff(); + // For UI 1.8, destroy must be invoked from the + // base widget + $.Widget.prototype.destroy.call( this ); + // For UI 1.9, define _destroy instead and don't + // worry about calling the base widget + }, + + methodB: function ( event ) { + //_trigger dispatches callbacks the plugin user can + // subscribe to + // signature: _trigger( "callbackName", [eventObject], + // [uiObject] ) + // e.g. this._trigger( "hover", e /*where e.type == + // "mouseenter"*/, { hovered: $(e.target)}); + this._trigger( "methodA", event, { + key: value + }); + }, + + methodA: function ( event ) { + this._trigger( "dataChanged", event, { + key: value + }); + }, + + // Respond to any changes the user makes to the option method + _setOption: function ( key, value ) { + switch ( key ) { + case "someValue": + // this.options.someValue = doSomethingWith( value ); + break; + default: + // this.options[ key ] = value; + break; + } + + // For UI 1.8, _setOption must be manually invoked from + // the base widget + $.Widget.prototype._setOption.apply(this, arguments); + // For UI 1.9 the _super method can be used instead + // this._super( "_setOption", key, value ); + } + }); + +})( jQuery, window, document ); + +//********************** Snippet 2 **********************// +var instance = $( "#foo" ).widgetName({ + foo: false +}); + +instance.widgetName( "methodB" ); + +//********************** Snippet 3 **********************// +$(document).on("pagecreate", function ( e ) { + // In here, e.target refers to the page that was created + // (it's the target of the pagecreate event) + // So, we can simply find elements on this page that match a + // selector of our choosing, and call our plugin on them. + // Here's how we'd call our "foo" plugin on any element with a + // data-role attribute of "foo": + $(e.target).find( "[data-role="foo"]" ).foo( options ); + + // Or, better yet, let's write the selector accounting for the configurable + // data-attribute namespace + $( e.target ).find( ":jqmData(role="foo")" ).foo( options ); +}); + + +//*******************************************************// +// RequireJS And The jQuery UI Widget Factory +//*******************************************************// + +//********************** Snippet 1 **********************// +/*! + * jQuery UI Widget + RequireJS module boilerplate (for 1.8/9+) + * Authors: @jrburke, @addyosmani + * Licensed under the MIT license + */ + +// Note from James: +// +// This assumes we are using the RequireJS+jQuery file, and +// that the following files are all in the same directory: +// +// - require-jquery.js +// - jquery-ui.custom.min.js (custom jQuery UI build with widget factory) +// - templates/ +// - asset.html +// - ao.myWidget.js + +// Then we can construct the widget as follows: + +// ao.myWidget.js file: +define( "ao.myWidget", ["jquery", "text!templates/asset.html", "underscore", "jquery-ui.custom.min"], function ( $, assetHtml, _ ) { + + // define our widget under a namespace of our choice + // "ao" is used here as a demonstration + $.widget( "ao.myWidget", { + + // Options to be used as defaults + options: {}, + + // Set up widget (e.g. create element, apply theming, + // bind events, etc.) + _create: function () { + + // _create will automatically run the first time + // this widget is called. Put the initial widget + // set-up code here, then we can access the element + // on which the widget was called via this.element. + // The options defined above can be accessed via + // this.options + + // this.element.addStuff(); + // this.element.addStuff(); + + // We can then use Underscore templating with + // with the assetHtml that has been pulled in + // var template = _.template( assetHtml ); + // this.content.append( template({}) ); + }, + + // Destroy an instantiated plugin and clean up modifications + // that the widget has made to the DOM + destroy: function () { + // this.element.removeStuff(); + // For UI 1.8, destroy must be invoked from the base + // widget + $.Widget.prototype.destroy.call( this ); + // For UI 1.9, define _destroy instead and don't worry + // about calling the base widget + }, + + methodB: function ( event ) { + // _trigger dispatches callbacks the plugin user can + // subscribe to + // signature: _trigger( "callbackName", [eventObject], + // [uiObject] ) + this._trigger( "methodA", event, { + key: value + }); + }, + + methodA: function ( event ) { + this._trigger("dataChanged", event, { + key: value + }); + }, + + // Respond to any changes the user makes to the option method + _setOption: function ( key, value ) { + switch (key) { + case "someValue": + // this.options.someValue = doSomethingWith( value ); + break; + default: + // this.options[ key ] = value; + break; + } + + // For UI 1.8, _setOption must be manually invoked from + // the base widget + $.Widget.prototype._setOption.apply( this, arguments ); + // For UI 1.9 the _super method can be used instead + // this._super( "_setOption", key, value ); + } + + }); +}); + +//********************** Snippet 2 **********************// + + +//********************** Snippet 3 **********************// +require({ + + paths: { + "jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min", + "jqueryui": "https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.18/jquery-ui.min", + "boilerplate": "../patterns/jquery.widget-factory.requirejs.boilerplate" + } +}, ["require", "jquery", "jqueryui", "boilerplate"], +function (req, $) { + + $(function () { + + var instance = $("#elem").myWidget(); + instance.myWidget("methodB"); + + }); +}); + + +//*******************************************************// +// Globally And Per-Call Overridable Options (Best Options Pattern) +//*******************************************************// + +//********************** Snippet 1 **********************// +/*! + * jQuery "best options" plugin boilerplate + * Author: @cowboy + * Further changes: @addyosmani + * Licensed under the MIT license + */ + +;(function ( $, window, document, undefined ) { + + $.fn.pluginName = function ( options ) { + + // Here's a best practice for overriding "defaults" + // with specified options. Note how, rather than a + // regular defaults object being passed as the second + // parameter, we instead refer to $.fn.pluginName.options + // explicitly, merging it with the options passed directly + // to the plugin. This allows us to override options both + // globally and on a per-call level. + + options = $.extend( {}, $.fn.pluginName.options, options ); + + return this.each(function () { + + var elem = $(this); + + }); + }; + + // Globally overriding options + // Here are our publicly accessible default plugin options + // that are available in case the user doesn't pass in all + // of the values expected. The user is given a default + // experience but can also override the values as necessary. + // e.g. $fn.pluginName.key ="otherval"; + + $.fn.pluginName.options = { + + key: "value", + myMethod: function ( elem, param ) { + + } + }; + +})( jQuery, window, document ); + +//********************** Snippet 2 **********************// +$("#elem").pluginName({ + key: "foobar" +}); + + +//*******************************************************// +// A Highly Configurable And Mutable Plugin Pattern +//*******************************************************// + +//********************** Snippet 1 **********************// +$( ".item-a" ).draggable( {"defaultPosition":"top-left"} ); +$( ".item-b" ).draggable( {"defaultPosition":"bottom-right"} ); +$( ".item-c" ).draggable( {"defaultPosition":"bottom-left"} ); +//etc + +//********************** Snippet 2 **********************// +$( ".items" ).draggable(); + +//********************** Snippet 3 **********************// +html +
    10. +
    11. + +//********************** Snippet 4 **********************// +/* + * "Highly configurable" mutable plugin boilerplate + * Author: @markdalgleish + * Further changes, comments: @addyosmani + * Licensed under the MIT license + */ + + +// Note that with this pattern, as per Alex Sexton's, the plugin logic +// hasn't been nested in a jQuery plugin. Instead, we just use +// jQuery for its instantiation. + +;(function( $, window, document, undefined ){ + + // our plugin constructor + var Plugin = function( elem, options ){ + this.elem = elem; + this.$elem = $(elem); + this.options = options; + + // This next line takes advantage of HTML5 data attributes + // to support customization of the plugin on a per-element + // basis. For example, + //
      + this.metadata = this.$elem.data( "plugin-options" ); + }; + + // the plugin prototype + Plugin.prototype = { + defaults: { + message: "Hello world!" + }, + + init: function() { + // Introduce defaults that can be extended either + // globally or using an object literal. + this.config = $.extend( {}, this.defaults, this.options, + this.metadata ); + + // Sample usage: + // Set the message per instance: + // $( "#elem" ).plugin( { message: "Goodbye World!"} ); + // or + // var p = new Plugin( document.getElementById( "elem" ), + // { message: "Goodbye World!"}).init() + // or, set the global default message: + // Plugin.defaults.message = "Goodbye World!" + + this.sampleMethod(); + return this; + }, + + sampleMethod: function() { + // e.g. show the currently configured message + // console.log(this.config.message); + } + } + + Plugin.defaults = Plugin.prototype.defaults; + + $.fn.plugin = function( options ) { + return this.each(function() { + new Plugin( this, options ).init(); + }); + }; + + // optional: window.Plugin = Plugin; + +})( jQuery, window, document ); + +//********************** Snippet 5 **********************// +$("#elem").plugin({ + message: "foobar" +}); diff --git a/book/snippets/05-Namespacing-Patterns/31-namespacing-fundamentals.es2015.js b/book/snippets/05-Namespacing-Patterns/31-namespacing-fundamentals.es2015.js new file mode 100644 index 00000000..8abba0f9 --- /dev/null +++ b/book/snippets/05-Namespacing-Patterns/31-namespacing-fundamentals.es2015.js @@ -0,0 +1,399 @@ +//*******************************************************// +// 1. Single global variables +//*******************************************************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax + +//********************** Snippet 1 **********************// +const myApplication = (() => ({ + function() { + //... + }, +}))(); + + +//*******************************************************// +// 2. Prefix namespacing +//*******************************************************// +// [ES2015+] We used new keyword const for immutable constant declaration + +//********************** Snippet 1 **********************// +const myApplication_propertyA = {}; +const myApplication_propertyB = {}; +function myApplication_myMethod() { + //... +} + +//*******************************************************// +// 3. Object literal notation +//*******************************************************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new object method declaration + +//********************** Snippet 1 **********************// +const myApplication = { + // As we've seen, we can easily define functionality for + // this object literal.. + getInfo() { + //... + }, + + // but we can also populate it to support + // further object namespaces containing anything + // anything we wish: + models: {}, + views: { + pages: {}, + }, + collections: {}, +}; + +//********************** Snippet 2 **********************// +// [ES2015+] We used new arrow function syntax +// [ES2015+] We used new object method declaration +// [ES2015+] Equivalent to: => { return "bar"; } +myApplication.foo = () => 'bar'; + +myApplication.utils = { + toString() { + //... + }, + export() { + //... + }, +}; + + +//********************** Snippet 3 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration + +// This doesn't check for existence of "myApplication" in +// the global namespace. Bad practice as we can easily +// clobber an existing variable/namespace with the same name +const myApplication = {}; + +// The following options *do* check for variable/namespace existence. +// If already defined, we use that instance, otherwise we assign a new +// object literal to myApplication. +// +// Option 1: const myApplication = myApplication || {}; +// Option 2: if( !MyApplication ){ MyApplication = {} }; +// Option 3: window.myApplication || ( window.myApplication = {} ); +// Option 4: const myApplication = $.fn.myApplication = function() {}; +// Option 5: const myApplication = myApplication === undefined ? {} : myApplication; + +//********************** Snippet 4 **********************// +myApplication || (myApplication = {}); + +//********************** Snippet 5 **********************// +// [ES2015+] We used new arrow function syntax + +const foo = () => { + myApplication || (myApplication = {}); +} + +// myApplication hasn't been initialized, +// so foo() throws a ReferenceError + +foo(); + +// However accepting myApplication as an +// argument + +const foo = (myApplication) => { + myApplication || (myApplication = {}); +} + +foo(); + + +// Even if myApplication === undefined, there is no error +// and myApplication gets set to {} correctly + +//********************** Snippet 6 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration + +// If we were to define a new plugin.. +const myPlugin = $.fn.myPlugin = () => { ... }; + +// Then later rather than having to type: +$.fn.myPlugin.defaults = {}; + +// We can do: +myPlugin.defaults = {}; + +//********************** Snippet 7 **********************// +// [ES2015+] We used new arrow function syntax +// [ES2015+] We used new keyword const for immutable constant declaration + +const namespace = ((() => { + // defined within the local scope + const privateMethod1 = () => { /* ... */ }; + + const privateMethod2 = () => { /* ... */ }; + const privateProperty1 = "foobar"; + + return { + + // the object literal returned here can have as many + // nested depths as we wish, however as mentioned, + // this way of doing things works best for smaller, + // limited-scope applications in my personal opinion + publicMethod1: privateMethod1, + + // nested namespace with public properties + properties: { + publicProperty1: privateProperty1 + }, + + // another tested namespace + utils: { + publicMethod2: privateMethod2 + } + ... + } +}))(); + +//********************** Snippet 8 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration + +const myConfig = { + language: 'english', + + defaults: { + enableGeolocation: true, + enableSharing: false, + maxPhotos: 20, + }, + + theme: { + skin: 'a', + toolbars: { + index: 'ui-navigation-toolbar', + pages: 'ui-custom-toolbar', + }, + }, +}; + + +//*******************************************************// +// 4. Nested namespacing +//*******************************************************// +// [ES2015+] We used new keyword const for immutable constant declaration + +//********************** Snippet 1 **********************// +YAHOO.util.Dom.getElementsByClassName('test'); + +//********************** Snippet 2 **********************// +const myApp = myApp || {}; + +// perform a similar existence check when defining nested +// children +myApp.routers = myApp.routers || {}; +myApp.model = myApp.model || {}; +myApp.model.special = myApp.model.special || {}; + +// nested namespaces can be as complex as required: +// myApp.utilities.charting.html5.plotGraph(/*..*/); +// myApp.modules.financePlanner.getSummary(); +// myApp.services.social.facebook.realtimeStream.getLatest(); + +//********************** Snippet 3 **********************// +myApp['routers'] = myApp['routers'] || {}; +myApp['models'] = myApp['models'] || {}; +myApp['controllers'] = myApp['controllers'] || {}; + + +//*******************************************************// +// 5. Immediately-invoked Function Expressions (IIFE)s +//*******************************************************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax + +//********************** Snippet 1 **********************// +// an (anonymous) immediately-invoked function expression +(() => { + /*...*/ +})(); + +// a named immediately-invoked function expression +(function foobar() { + /*..*/ +})(); + +//********************** Snippet 2 **********************// +// named self-executing function +function foobar() { + foobar(); +} + +// anonymous self-executing function +// [ES2015+] The rest parameter syntax allows us to represent an indefinite number of arguments as an array. + +const foobar = (...args) => { + args.callee(); +}; + +//********************** Snippet 3 **********************// +const namespace = namespace || {}; + +// here a namespace object is passed as a function +// parameter, where we assign public methods and +// properties to it +(o => { + o.foo = 'foo'; + // [ES2015+] Equivalent to: => { return "bar"; } + o.bar = () => 'bar'; +})(namespace); + +console.log(namespace); + + +//********************** Snippet 4 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax + +// namespace (our namespace name) and undefined are passed here +// to ensure 1. namespace can be modified locally and isn't +// overwritten outside of our function context +// 2. the value of undefined is guaranteed as being truly +// undefined. This is to avoid issues with undefined being +// mutable pre-ES5. + +;((namespace, undefined) => { + // private properties + const foo = 'foo'; + + const bar = 'bar'; + + // public methods and properties + namespace.foobar = 'foobar'; + + namespace.say = msg => { + speak(msg); + }; + + namespace.sayHello = () => { + namespace.say('hello world'); + }; + + // private method + function speak(msg) { + console.log(`You said: ${msg}`); + } +})((window.namespace = window.namespace || {})); + +// we can then test our properties and methods as follows + +// public + +// Outputs: foobar +console.log(namespace.foobar); + +// Outputs: You said: hello world +namespace.sayHello(); + +// assigning new properties +namespace.foobar2 = 'foobar'; + +// Outputs: foobar +console.log(namespace.foobar2); + + +//********************** Snippet 5 **********************// +// [ES2015+] We used new arrow function syntax + +// let's extend the namespace with new functionality +((namespace, undefined) => { + // public method + namespace.sayGoodbye = () => { + namespace.say('goodbye'); + }; +})((window.namespace = window.namespace || {})); + +// Outputs: goodbye +namespace.sayGoodbye(); + + + +//*******************************************************// +// 6. Namespace injection +//*******************************************************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [SE2015+] We used new keyword let, which declares a block scope local variable +// [ES2015+] We used new arrow function syntax + +//********************** Snippet 1 **********************// +const myApp = myApp || {}; +myApp.utils = {}; + +(function() { + let val = 5; + + this.getValue = () => val; + + this.setValue = newVal => { + val = newVal; + }; + + // also introduce a new sub-namespace + this.tools = {}; +}.apply(myApp.utils)); + +// inject new behaviour into the tools namespace +// which we defined via the utilities module + +(function() { + this.diagnose = () => 'diagnosis'; +}.apply(myApp.utils.tools)); + +// note, this same approach to extension could be applied +// to a regular IIFE, by just passing in the context as +// an argument and modifying the context rather than just +// "this" + +// Usage: + +// Outputs our populated namespace +console.log(myApp); + +// Outputs: 5 +console.log(myApp.utils.getValue()); + +// Sets the value of `val` and returns it +myApp.utils.setValue(25); +console.log(myApp.utils.getValue()); + +// Testing another level down +console.log(myApp.utils.tools.diagnose()); + + +//********************** Snippet 2 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax +// [SE2015+] We used new keyword let, which declares a block scope local variable + +// define a namespace we can use later +const ns = ns || {}; + +const ns2 = ns2 || {}; + +// the module/namespace creator +const creator = function(val) { + let val = val || 0; + + this.next = () => val++; + + this.reset = () => { + val = 0; + }; +}; + +creator.call(ns); + +// ns.next, ns.reset now exist +creator.call(ns2, 5000); + +// ns2 contains the same methods +// but has an overridden value for val +// of 5000 diff --git a/book/snippets/05-Namespacing-Patterns/31-namespacing-fundamentals.es5.js b/book/snippets/05-Namespacing-Patterns/31-namespacing-fundamentals.es5.js new file mode 100644 index 00000000..f6d74695 --- /dev/null +++ b/book/snippets/05-Namespacing-Patterns/31-namespacing-fundamentals.es5.js @@ -0,0 +1,371 @@ +//*******************************************************// +// 1. Single global variables +//*******************************************************// + +//********************** Snippet 1 **********************// +var myApplication = (function () { + function() { + //... + }, + return { + //... + } +})(); + + +//*******************************************************// +// 2. Prefix namespacing +//*******************************************************// + +//********************** Snippet 1 **********************// +var myApplication_propertyA = {}; +var myApplication_propertyB = {}; +function myApplication_myMethod() { + //... +} + + +//*******************************************************// +// 3. Object literal notation +//*******************************************************// + +//********************** Snippet 1 **********************// +var myApplication = { + + // As we've seen, we can easily define functionality for + // this object literal.. + getInfo: function() { + //... + }, + + // but we can also populate it to support + // further object namespaces containing anything + // anything we wish: + models: {}, + views: { + pages: {} + }, + collections: {} +}; + +//********************** Snippet 2 **********************// +myApplication.foo = function() { + return "bar"; +} + +myApplication.utils = { + toString: function() { + //... + }, + export: function() { + //... + } +} + +//********************** Snippet 3 **********************// +// This doesn't check for existence of "myApplication" in +// the global namespace. Bad practice as we can easily +// clobber an existing variable/namespace with the same name +var myApplication = {}; + +// The following options *do* check for variable/namespace existence. +// If already defined, we use that instance, otherwise we assign a new +// object literal to myApplication. +// +// Option 1: var myApplication = myApplication || {}; +// Option 2: if( !MyApplication ){ MyApplication = {} }; +// Option 3: window.myApplication || ( window.myApplication = {} ); +// Option 4: var myApplication = $.fn.myApplication = function() {}; +// Option 5: var myApplication = myApplication === undefined ? {} : myApplication; + +//********************** Snippet 4 **********************// +myApplication || (myApplication = {}); + +//********************** Snippet 5 **********************// +function foo() { + myApplication || ( myApplication = {} ); +} + +// myApplication hasn't been initialized, +// so foo() throws a ReferenceError + +foo(); + +// However accepting myApplication as an +// argument + +function foo( myApplication ) { + myApplication || ( myApplication = {} ); +} + +foo(); + +// Even if myApplication === undefined, there is no error +// and myApplication gets set to {} correctly + +//********************** Snippet 6 **********************// +// If we were to define a new plugin.. +var myPlugin = $.fn.myPlugin = function() { ... }; + +// Then later rather than having to type: +$.fn.myPlugin.defaults = {}; + +// We can do: +myPlugin.defaults = {}; + +//********************** Snippet 7 **********************// +var namespace = (function () { + + // defined within the local scope + var privateMethod1 = function () { /* ... */ }, + privateMethod2 = function () { /* ... */ }, + privateProperty1 = "foobar"; + +return { + + // the object literal returned here can have as many + // nested depths as we wish, however as mentioned, + // this way of doing things works best for smaller, + // limited-scope applications in my personal opinion + publicMethod1: privateMethod1, + + // nested namespace with public properties + properties: { + publicProperty1: privateProperty1 + }, + + // another tested namespace + utils: { + publicMethod2: privateMethod2 + } + ... +} +})(); + +//********************** Snippet 8 **********************// +var myConfig = { + + language: "english", + + defaults: { + enableGeolocation: true, + enableSharing: false, + maxPhotos: 20 + }, + + theme: { + skin: "a", + toolbars: { + index: "ui-navigation-toolbar", + pages: "ui-custom-toolbar" + } + } +} + +//*******************************************************// +// 4. Nested namespacing +//*******************************************************// + +//********************** Snippet 1 **********************// +YAHOO.util.Dom.getElementsByClassName("test"); + +//********************** Snippet 2 **********************// +var myApp = myApp || {}; + +// perform a similar existence check when defining nested +// children +myApp.routers = myApp.routers || {}; +myApp.model = myApp.model || {}; +myApp.model.special = myApp.model.special || {}; + +// nested namespaces can be as complex as required: +// myApp.utilities.charting.html5.plotGraph(/*..*/); +// myApp.modules.financePlanner.getSummary(); +// myApp.services.social.facebook.realtimeStream.getLatest(); + +//********************** Snippet 3 **********************// +myApp["routers"] = myApp["routers"] || {}; +myApp["models"] = myApp["models"] || {}; +myApp["controllers"] = myApp["controllers"] || {}; + + +//*******************************************************// +// 5. Immediately-invoked Function Expressions (IIFE)s +//*******************************************************// + +//********************** Snippet 1 **********************// +// an (anonymous) immediately-invoked function expression +(function () { /*...*/ })(); + +// a named immediately-invoked function expression +(function foobar () { /*..*/ })(); + +//********************** Snippet 2 **********************// +// named self-executing function +function foobar () { foobar(); } + +// anonymous self-executing function +var foobar = function () { arguments.callee(); } + +//********************** Snippet 3 **********************// +var namespace = namespace || {}; + +// here a namespace object is passed as a function +// parameter, where we assign public methods and +// properties to it +(function( o ) { + o.foo = "foo"; + o.bar = function() { + return "bar"; + }; +})( namespace ); + +console.log( namespace ); + +//********************** Snippet 4 **********************// +// namespace (our namespace name) and undefined are passed here +// to ensure 1. namespace can be modified locally and isn't +// overwritten outside of our function context +// 2. the value of undefined is guaranteed as being truly +// undefined. This is to avoid issues with undefined being +// mutable pre-ES5. + +;(function ( namespace, undefined ) { + + // private properties + var foo = "foo", + bar = "bar"; + + // public methods and properties + namespace.foobar = "foobar"; + + namespace.say = function ( msg ) { + speak( msg ); + }; + + namespace.sayHello = function () { + namespace.say( "hello world" ); + }; + + // private method + function speak(msg) { + console.log( "You said: " + msg ); + }; + + // check to evaluate whether "namespace" exists in the + // global namespace - if not, assign window.namespace an + // object literal + +})( window.namespace = window.namespace || {} ); + + +// we can then test our properties and methods as follows + +// public + +// Outputs: foobar +console.log( namespace.foobar ); + +// Outputs: You said: hello world +namespace.sayHello(); + +// assigning new properties +namespace.foobar2 = "foobar"; + +// Outputs: foobar +console.log( namespace.foobar2 ); + +//********************** Snippet 5 **********************// +// let's extend the namespace with new functionality +(function( namespace, undefined ) { + + // public method + namespace.sayGoodbye = function () { + namespace.say( "goodbye" ); + } +})( window.namespace = window.namespace || {}); + +// Outputs: goodbye +namespace.sayGoodbye(); + + +//*******************************************************// +// 6. Namespace injection +//*******************************************************// + +//********************** Snippet 1 **********************// +var myApp = myApp || {}; +myApp.utils = {}; + +(function () { + var val = 5; + + this.getValue = function () { + return val; + }; + + this.setValue = function( newVal ) { + val = newVal; + } + + // also introduce a new sub-namespace + this.tools = {}; + +}).apply( myApp.utils ); + +// inject new behaviour into the tools namespace +// which we defined via the utilities module + +(function () { + this.diagnose = function() { + return "diagnosis"; + } +}).apply( myApp.utils.tools ); + +// note, this same approach to extension could be applied +// to a regular IIFE, by just passing in the context as +// an argument and modifying the context rather than just +// "this" + +// Usage: + +// Outputs our populated namespace +console.log( myApp ); + +// Outputs: 5 +console.log( myApp.utils.getValue() ); + +// Sets the value of `val` and returns it +myApp.utils.setValue( 25 ); +console.log( myApp.utils.getValue() ); + +// Testing another level down +console.log( myApp.utils.tools.diagnose() ); + +//********************** Snippet 2 **********************// +// define a namespace we can use later +var ns = ns || {}, + ns2 = ns2 || {}; + +// the module/namespace creator +var creator = function( val ) { + + var val = val || 0; + + this.next = function () { + return val++ + }; + + this.reset = function () { + val = 0; + }; +}; + +creator.call( ns ); + +// ns.next, ns.reset now exist +creator.call( ns2, 5000 ); + +// ns2 contains the same methods +// but has an overridden value for val +// of 5000 diff --git a/book/snippets/05-Namespacing-Patterns/32-advanced-namespacing-patterns.es2015.js b/book/snippets/05-Namespacing-Patterns/32-advanced-namespacing-patterns.es2015.js new file mode 100644 index 00000000..1e3a30de --- /dev/null +++ b/book/snippets/05-Namespacing-Patterns/32-advanced-namespacing-patterns.es2015.js @@ -0,0 +1,218 @@ +//*******************************************************// +// Automating nested namespacing +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration + +const application = { + utilities: { + drawing: { + canvas: { + to2d: { + //... + }, + }, + }, + }, +}; + +//********************** Snippet 2 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax +// [SE2015+] We used new keyword let, which declares a block scope local variable + +// top-level namespace being assigned an object literal +const myApp = myApp || {}; + +// a convenience function for parsing string namespaces and +// automatically generating nested namespaces +const extend = (ns, ns_string) => { + const parts = ns_string.split('.'); + let parent = ns; + let pl; + + pl = parts.length; + + for (let i = 0; i < pl; i++) { + // create a property if it doesn't exist + if (typeof parent[parts[i]] === 'undefined') { + parent[parts[i]] = {}; + } + + parent = parent[parts[i]]; + } + + return parent; +} + +// Usage: +// extend myApp with a deeply nested namespace +const mod = extend(myApp, 'modules.module2'); + +// the correct object with nested depths is output +console.log(mod); + +// minor test to check the instance of mod can also +// be used outside of the myApp namesapce as a clone +// that includes the extensions + +// Outputs: true +console.log(mod == myApp.modules.module2); + +// further demonstration of easier nested namespace +// assignment using extend +extend(myApp, 'moduleA.moduleB.moduleC.moduleD'); +extend(myApp, 'longer.version.looks.like.this'); +console.log(myApp); + + +//*******************************************************// +// Dependency declaration pattern +//*******************************************************// +// [ES2015+] We used new keyword const for immutable constant declaration + +//********************** Snippet 1 **********************// +// common approach to accessing nested namespaces +myApp.utilities.math.fibonacci(25); +myApp.utilities.math.sin(56); +myApp.utilities.drawing.plot(98, 50, 60); + +// with local/cached references +const utils = myApp.utilities; + +const maths = utils.math; +const drawing = utils.drawing; + +// easier to access the namespace +maths.fibonacci(25); +maths.sin(56); +drawing.plot(98, 50, 60); + +// note that the above is particularly performant when +// compared to hundreds or thousands of calls to nested +// namespaces vs. a local reference to the namespace + + +//*******************************************************// +// Deep object extension +//*******************************************************// + +//********************** Snippet 1 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new arrow function syntax +// [ES2015+] We used new object method declaration + +// extend.js +// Written by Andrew Dupont, optimized by Addy Osmani + +const extend = (destination, source) => { + const toString = Object.prototype.toString; + const objTest = toString.call({}); + + for (const property in source) { + if (source[property] && objTest === toString.call(source[property])) { + destination[property] = destination[property] || {}; + extend(destination[property], source[property]); + } else { + destination[property] = source[property]; + } + } + + return destination; +} + +console.group('objExtend namespacing tests'); + +// define a top-level namespace for usage +const myNS = myNS || {}; + +// 1. extend namespace with a "utils" object +extend(myNS, { + utils: { + //... + }, +}); + +console.log('test 1', myNS); +// myNS.utils now exists + +// 2. extend with multiple depths (namespace.hello.world.wave) +extend(myNS, { + hello: { + world: { + wave: { + test() { + //... + }, + }, + }, + }, +}); + +// test direct assignment works as expected +myNS.hello.test1 = 'this is a test'; +myNS.hello.world.test2 = 'this is another test'; +console.log('test 2', myNS); + +// 3. what if myNS already contains the namespace being added +// (e.g. "library")? we want to ensure no namespaces are being +// overwritten during extension + +myNS.library = { + foo() {}, +}; + +extend(myNS, { + library: { + bar() { + //... + }, + }, +}); + +// confirmed that extend is operating safely (as expected) +// myNS now also contains library.foo, library.bar +console.log('test 3', myNS); + +// 4. what if we wanted easier access to a specific namespace without having +// to type the whole namespace out each time? + +const shorterAccess1 = myNS.hello.world; +shorterAccess1.test3 = 'hello again'; +console.log('test 4', myNS); + +//success, myApp.hello.world.test3 is now "hello again" + +console.groupEnd(); + + +//********************** Snippet 2 **********************// +// [ES2015+] We used new keyword const for immutable constant declaration +// [ES2015+] We used new object method declaration + +// top-level namespace +const myApp = myApp || {}; + +// directly assign a nested namespace +myApp.library = { + foo() { + //... + }, +}; + +// deep extend/merge this namespace with another +// to make things interesting, let's say it's a namespace +// with the same name but with a different function +// signature: $.extend( deep, target, object1, object2 ) +$.extend(true, myApp, { + library: { + bar() { + //... + }, + }, +}); + +console.log('test', myApp); +// myApp now contains both library.foo() and library.bar() methods +// nothing has been overwritten which is what we're hoping for. diff --git a/book/snippets/05-Namespacing-Patterns/32-advanced-namespacing-patterns.es5.js b/book/snippets/05-Namespacing-Patterns/32-advanced-namespacing-patterns.es5.js new file mode 100644 index 00000000..32634ed7 --- /dev/null +++ b/book/snippets/05-Namespacing-Patterns/32-advanced-namespacing-patterns.es5.js @@ -0,0 +1,203 @@ +//*******************************************************// +// Automating nested namespacing +//*******************************************************// + +//********************** Snippet 1 **********************// +var application = { + utilities: { + drawing: { + canvas: { + 2d:{ + //... + } + } + } + } +}; + +//********************** Snippet 2 **********************// +// top-level namespace being assigned an object literal +var myApp = myApp || {}; + +// a convenience function for parsing string namespaces and +// automatically generating nested namespaces +function extend( ns, ns_string ) { + var parts = ns_string.split("."), + parent = ns, + pl; + + pl = parts.length; + + for ( var i = 0; i < pl; i++ ) { + // create a property if it doesn't exist + if ( typeof parent[parts[i]] === "undefined" ) { + parent[parts[i]] = {}; + } + + parent = parent[parts[i]]; + } + + return parent; +} + +// Usage: +// extend myApp with a deeply nested namespace +var mod = extend(myApp, "modules.module2"); + +// the correct object with nested depths is output +console.log(mod); + +// minor test to check the instance of mod can also +// be used outside of the myApp namesapce as a clone +// that includes the extensions + +// Outputs: true +console.log(mod == myApp.modules.module2); + +// further demonstration of easier nested namespace +// assignment using extend +extend(myApp, "moduleA.moduleB.moduleC.moduleD"); +extend(myApp, "longer.version.looks.like.this"); +console.log(myApp); + + +//*******************************************************// +// Dependency declaration pattern +//*******************************************************// + +//********************** Snippet 1 **********************// +// common approach to accessing nested namespaces +myApp.utilities.math.fibonacci( 25 ); +myApp.utilities.math.sin( 56 ); +myApp.utilities.drawing.plot( 98,50,60 ); + +// with local/cached references +var utils = myApp.utilities, +maths = utils.math, +drawing = utils.drawing; + +// easier to access the namespace +maths.fibonacci( 25 ); +maths.sin( 56 ); +drawing.plot( 98, 50,60 ); + +// note that the above is particularly performant when +// compared to hundreds or thousands of calls to nested +// namespaces vs. a local reference to the namespace + + +//*******************************************************// +// Deep object extension +//*******************************************************// + +//********************** Snippet 1 **********************// +// extend.js +// Written by Andrew Dupont, optimized by Addy Osmani + +function extend( destination, source ) { + + var toString = Object.prototype.toString, + objTest = toString.call({}); + + for ( var property in source ) { + if ( source[property] && objTest === toString.call(source[property]) ) { + destination[property] = destination[property] || {}; + extend(destination[property], source[property]); + } else { + destination[property] = source[property]; + } + } + + return destination; +}; + +console.group( "objExtend namespacing tests" ); + +// define a top-level namespace for usage +var myNS = myNS || {}; + +// 1. extend namespace with a "utils" object +extend(myNS, { + utils:{ + //... + } +}); + +console.log( "test 1", myNS); +// myNS.utils now exists + +// 2. extend with multiple depths (namespace.hello.world.wave) +extend(myNS, { + hello: { + world: { + wave: { + test: function() { + //... + } + } + } + } +}); + +// test direct assignment works as expected +myNS.hello.test1 = "this is a test"; +myNS.hello.world.test2 = "this is another test"; +console.log( "test 2", myNS ); + +// 3. what if myNS already contains the namespace being added +// (e.g. "library")? we want to ensure no namespaces are being +// overwritten during extension + +myNS.library = { + foo:function () {} +}; + +extend( myNS, { + library: { + bar: function() { + //... + } + } +}); + +// confirmed that extend is operating safely (as expected) +// myNS now also contains library.foo, library.bar +console.log( "test 3", myNS ); + +// 4. what if we wanted easier access to a specific namespace without having +// to type the whole namespace out each time? + +var shorterAccess1 = myNS.hello.world; +shorterAccess1.test3 = "hello again"; +console.log( "test 4", myNS); + +//success, myApp.hello.world.test3 is now "hello again" + +console.groupEnd(); + +//********************** Snippet 2 **********************// +// top-level namespace +var myApp = myApp || {}; + +// directly assign a nested namespace +myApp.library = { + foo: function() { + //... + } +}; + +// deep extend/merge this namespace with another +// to make things interesting, let's say it's a namespace +// with the same name but with a different function +// signature: $.extend( deep, target, object1, object2 ) +$.extend( true, myApp, { + library:{ + bar: function(){ + //... + } + } +}); + +console.log("test", myApp); +// myApp now contains both library.foo() and library.bar() methods +// nothing has been overwritten which is what we're hoping for. diff --git a/package-lock.json b/package-lock.json index 29dc7a75..2bbccd28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "essential-js-design-patterns", - "version": "1.6.2", + "version": "1.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -6788,12 +6788,6 @@ "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", "dev": true }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, "string-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", @@ -6820,6 +6814,12 @@ } } }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",