Skip to content

Instantly share code, notes, and snippets.

@smorcuend
Last active October 20, 2017 11:53
Show Gist options
  • Save smorcuend/ff93096253d2247a6e816603cec1e1fe to your computer and use it in GitHub Desktop.
Save smorcuend/ff93096253d2247a6e816603cec1e1fe to your computer and use it in GitHub Desktop.
Angular Model Pattern - use model in service
export class AuthApplicationService {
// inject all needed services
constructor(
private sessionService: SessionService,
private authService: AuthService
) {}
// orchestrate multi service execution
// note that subscription and cancelation are responsibility of the caller
login(username: string, password: string): Observable<boolean> {
return this.authService.login(username, password) // returns Observable<User>
.do(user => this.sessionService.setUser(user)) // .do() to perform side-effects, mutate model (sync)
.mapTo(true); // return boolean flag
.catch(err => this.sessionService.setUser(null)); // handle failure, mutate model (sync)
}
/* ... */
}
/* session and clients models in some component or service */
const filteredClients$ = this.sessionService.session$
.combineLatest(this.clientService.clients$)
.map(([session, clients]) => clients.filter(client => {
if (client.vip && session.accessVips) {
return true;
} else if (!client.vip) {
return true;
}
return false;
}));
export class TodosComponent {
/* ... */
onTodoToggleClick(todo: Todo) {
this.todosService.toggleTodo(todo.name);
}
}
@Component({
selector: 'todos',
template: `
<!--
accessing multiple properties,
we're better off subscribing only once in component's ngOnInit()
-->
Done todos: {{count.done}}
Remaining todos: {{count.remaining}}
All todos: {{count.all}}
<ul>
<!-- implicit subscription using | async pipe -->
<li *ngFor="let todo of todosService.todos$ | async;">
{{todo.name}}
</li>
</ul>
`
})
export class TodosComponent implements OnInit, OnDestroy {
subscription: Subscription;
counts: TodosCounts;
constructor(public todosService: TodosService) { }
ngOnInit() {
// explicit subscription using .subscribe()
this.subscription = this.todosService.todosCounts$
.subscribe(counts => (this.counts = counts)));
}
ngOnDestroy() {
subscription.unsubscribe();
}
}
/* route configuration */
const routes: Routes = [
{
path: 'todos',
component: TodosComponent,
resolve: {
todosInitialized: TodosService
}
}
/* ... */
];
/* business service */
@Injectable()
export class TodosService implements Resolve<boolean> {
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
// get data from http, websocket, localstorage, ...
// retrieve url state from route (query params, path params, ...)
// sync
const data = storage.get('my-data');
this.model.set(data); // set data in model
return Observable.of(true); // to finish navigation to the new route
// or async
return this.http.get(url)
.map(res => res.json()) // extract data from response
.do(data => this.model.set(data)) // set data in model
.mapTo(true); // return true if successful
}
}
@Injectable()
export class TodosService {
private model: Model<Todo[]>;
todos$: Observable<Todo[]>;
constructor(private modelFactory: ModelFactory<Todo[]>) {
this.model = this.modelFactory.create([]);
this.todos$ = this.model.data$;
}
}
export interface Todo {
name: string;
done: boolean;
}
@Injectable()
export class TodosService {
/* ... */
// pass all neccessary data as funcion parameters
toggleTodo(name: string) {
// retrieve model internal state
const todos = this.model.get();
// mutate model internal state
todos.forEach(t => {
if (t.name === name) {
t.done = !t.done;
}
});
// set new model state; and notify all subscribed componenets
this.model.set(todos);
}
}
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
export class Model<T> {
private _data: BehaviorSubject<T>;
data$: Observable<T>;
constructor(initialData: any, immutable: boolean, clone?: (data: T) => T) {
this._data = new BehaviorSubject(initialData);
this.data$ = this._data.asObservable()
.map(data => immutable
? clone ? clone(data) : JSON.parse(JSON.stringify(data))
: data);
}
get(): T {
return this._data.getValue();
}
set(data: T) {
this._data.next(data);
}
}
export class ModelFactory<T> {
create(initialData: T): Model<T> { return new Model<T>(initialData, true); }
createMutable(initialData: T): Model<T> {
return new Model<T>(initialData, false);
}
createWithCustomClone(initialData: T, clone: (data: T) => T) {
return new Model<T>(initialData, true, clone);
}
}
export function useModelFactory() {
return new ModelFactory();
}
export const MODEL_PROVIDER = {
provide: ModelFactory, useFactory: useModelFactory
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment