Skip to content

feat(message): Add notification functionality #241

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9b902dc
feat(message): Added very early implementation of message container
edcarroll Jun 8, 2017
1bff1c4
Merge branch 'master' into feat(message)/notification-functionality
edcarroll Jun 16, 2017
2899a87
style: Fixed tslint errors
edcarroll Jun 16, 2017
d5c0b77
feat(message): Migrated to generated components, similar to popup
edcarroll Jun 16, 2017
9a61406
refactor: Removed old message and replaced with new one
edcarroll Jun 16, 2017
0e7dc12
feat: Created internal component factory service
edcarroll Jun 16, 2017
4b27513
refactor(popup): Moved to new component factory service
edcarroll Jun 16, 2017
5fdd91a
refactor(message): Moved message container to component factory service
edcarroll Jun 16, 2017
602d121
feat: Added initial styles to message container
edcarroll Jun 16, 2017
5a2ca13
feat(message): Added queue functionality
edcarroll Jun 16, 2017
020f74e
feat(message): Removed reliance on `@ViewChild`
edcarroll Jun 16, 2017
9c150fc
feat(modal): Internal ActiveModal functionality now hidden from consumer
edcarroll Jun 16, 2017
772cf68
feat(message): Renamed isDismissable to hasDismissButton
edcarroll Jun 16, 2017
6627a22
feat(message): Added showNewestFirst property
edcarroll Jun 16, 2017
c55b03c
feat(message): Added manual dismiss method
edcarroll Jun 16, 2017
e85b519
feat(message): Added rough progress bar
edcarroll Jun 16, 2017
e0eb5e1
feat(progress): Added transition options and ability to completely em…
edcarroll Jun 16, 2017
2162a1b
style: Fixed tslint rule for public property names
edcarroll Jun 16, 2017
dd4878e
feat: Made internal properties of ActiveModal & ActiveMessage public
edcarroll Jun 16, 2017
9cb4412
feat(message): Added dismissAll method to controller
edcarroll Jun 16, 2017
84b1a4a
feat(message): Added global message service
edcarroll Jun 18, 2017
e7eaf13
feat(message): Added display functionality to global service
edcarroll Jun 18, 2017
1b70a7c
feat(message): Added global positioning support
edcarroll Jun 18, 2017
ae87f33
feat(message): Added global service accessors
edcarroll Jun 18, 2017
4491f15
docs: Minor modal & message updates
edcarroll Jun 18, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions components/message/active-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { MessageConfig } from "./message-config";
import { ComponentRef } from "@angular/core";
import { SuiMessage } from "./message";

export abstract class SuiActiveMessage {
public abstract onClick(callback:() => void):SuiActiveMessage;
public abstract onDismiss(callback:() => void):SuiActiveMessage;

public abstract dismiss():void;
}

export class ActiveMessage implements SuiActiveMessage {
public config:MessageConfig;
public componentRef:ComponentRef<SuiMessage>;

public get component():SuiMessage {
return this.componentRef.instance;
}

constructor(config:MessageConfig, componentRef:ComponentRef<SuiMessage>) {
this.config = config;
this.componentRef = componentRef;

this.component.onDismiss.subscribe(() => this.componentRef.destroy());
}

public onClick(callback:() => void):ActiveMessage {
this.config.onClick.subscribe(() => callback());
return this;
}

public onDismiss(callback:() => void):ActiveMessage {
this.config.onDismiss.subscribe(() => callback());
return this;
}

public dismiss():void {
this.component.dismiss();
}
}
49 changes: 49 additions & 0 deletions components/message/message-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { EventEmitter } from "@angular/core";

export type MessageState = "" | "info" | "success" | "warning" | "error";

export const MessageState = {
Default: "" as MessageState,
Info: "info" as MessageState,
Success: "success" as MessageState,
Warning: "warning" as MessageState,
Error: "error" as MessageState
};

export class MessageConfig {
public text:string;
public header:string;
public state:MessageState;

public timeout:number;
public extendedTimeout:number;

public hasDismissButton:boolean;
public hasProgress:boolean;

public transition:string;
public transitionInDuration:number;
public transitionOutDuration:number;

public onClick:EventEmitter<void>;
public onDismiss:EventEmitter<void>;

constructor(text:string, state:MessageState = MessageState.Default, header?:string) {
this.text = text;
this.state = state;
this.header = header;

this.timeout = 5000;
this.extendedTimeout = 1000;

this.hasDismissButton = true;
this.hasProgress = false;

this.transition = "fade";
this.transitionInDuration = 400;
this.transitionOutDuration = 1000;

this.onClick = new EventEmitter<void>();
this.onDismiss = new EventEmitter<void>();
}
}
94 changes: 94 additions & 0 deletions components/message/message-container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Component, EventEmitter, Input, ComponentFactoryResolver, ViewContainerRef, ViewChild, ElementRef } from "@angular/core";
import { MessageConfig } from "./message-config";
import { ActiveMessage, SuiActiveMessage } from "./active-message";
import { SuiMessage } from "./message";
import { SuiComponentFactory } from "../util/component-factory.service";
import { MessageController, IMessageController } from "./message-controller";

@Component({
selector: "sui-message-container",
template: `
<div #containerSibling></div>
`,
styles: [`
:host {
display: block;
}

:host >>> sui-message {
display: block;
margin-bottom: 1rem;
}

:host >>> sui-message:last-of-type {
margin-bottom: 0;
}

:host >>> sui-message {
cursor: pointer;
}
`]
})
export class SuiMessageContainer {
private _messages:ActiveMessage[];
private _queue:ActiveMessage[];

@Input()
public set controller(controller:MessageController) {
controller.registerContainer(this);
}

@ViewChild("containerSibling", { read: ViewContainerRef })
public containerSibling:ViewContainerRef;

constructor(private _componentFactory:SuiComponentFactory, private _element:ElementRef) {
this._messages = [];
this._queue = [];
}

public show(config:MessageConfig, maxShown:number, showNewestFirst:boolean):SuiActiveMessage {
const componentRef = this._componentFactory.createComponent(SuiMessage);
componentRef.instance.loadConfig(config);

const active = new ActiveMessage(config, componentRef)
.onDismiss(() => this.onMessageClose(active, showNewestFirst));

if (this._messages.length < maxShown) {
this.open(active, showNewestFirst);
} else {
this.queue(active);
}

return active;
}

private open(message:ActiveMessage, showNewestFirst:boolean):void {
this._messages.push(message);

this._componentFactory.attachToView(message.componentRef, this.containerSibling);
if (!showNewestFirst) {
this._componentFactory.moveToElement(message.componentRef, this._element.nativeElement);
}

message.component.show();
}

private queue(message:ActiveMessage):void {
this._queue.push(message);
}

public dismissAll():void {
this._queue = [];
this._messages.forEach(m => m.dismiss());
}

private onMessageClose(message:ActiveMessage, showNewestFirst:boolean):void {
this._messages = this._messages.filter(m => m !== message);

if (this._queue.length > 0) {
const queued = this._queue.shift();

this.open(queued, showNewestFirst);
}
}
}
44 changes: 44 additions & 0 deletions components/message/message-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { MessageConfig } from "./message-config";
import { SuiActiveMessage } from "./active-message";
import { SuiMessageContainer } from "./message-container";

export interface IMessageController {
maxShown:number;
isNewestOnTop:boolean;
show(config:MessageConfig):SuiActiveMessage;
dismissAll():void;
}

export class MessageController implements IMessageController {
private _container:SuiMessageContainer;

public maxShown:number;
public isNewestOnTop:boolean;

constructor() {
this.maxShown = 7;
this.isNewestOnTop = true;
}

public registerContainer(container:SuiMessageContainer):void {
this._container = container;
}

public show(config:MessageConfig):SuiActiveMessage {
this.throwContainerError();

return this._container.show(config, this.maxShown, this.isNewestOnTop);
}

public dismissAll():void {
this.throwContainerError();

return this._container.dismissAll();
}

private throwContainerError():void {
if (!this._container) {
throw new Error("You must pass this controller to a message container.");
}
}
}
86 changes: 86 additions & 0 deletions components/message/message-global-container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@

import { Component, HostListener } from "@angular/core";
import { MessageController } from "./message-controller";
import { SuiMessageService } from "./message-service";
import { IDynamicClasslist } from "../util/interfaces";
import { getDocumentFontSize } from "../util/util";

export type MessagePosition = "top" | "top-left" | "top-right" |
"bottom" | "bottom-left" | "bottom-right";

export const MessagePosition = {
Top: "top" as MessagePosition,
TopLeft: "top-left" as MessagePosition,
TopRight: "top-right" as MessagePosition,
Bottom: "bottom" as MessagePosition,
BottomLeft: "bottom-left" as MessagePosition,
BottomRight: "bottom-right" as MessagePosition
};

@Component({
selector: "sui-message-global-container",
template: `
<div class="global container" [ngClass]="dynamicClasses" [style.width.px]="dynamicWidth">
<sui-message-container [controller]="controller"></sui-message-container>
</div>
`,
styles: [`
.global.container {
display: block;
position: fixed;
}

.global.container.top {
top: 1rem;
}

.global.container.bottom {
bottom: 1rem;
}

.global.container.left {
left: 1rem;
}

.global.container.right {
right: 1rem;
}

.global.container:not(.left):not(.right) {
left: 1rem;
}
`]
})
export class SuiMessageGlobalContainer {
public controller:MessageController;

public position:MessagePosition;
public width:number;

public get dynamicClasses():IDynamicClasslist {
const classes:IDynamicClasslist = {};

this.position
.split("-")
.forEach(p => classes[p] = true);

return classes;
}

public get dynamicWidth():number {
const margin = getDocumentFontSize();
let width = this.width;

if (this.position === MessagePosition.Top ||
this.position === MessagePosition.Bottom ||
window.innerWidth < width + margin * 2) {

width = window.innerWidth - margin * 2;
}

return width;
}

@HostListener("window:resize")
public onDocumentResize():void {}
}
69 changes: 69 additions & 0 deletions components/message/message-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Injectable, ComponentRef } from "@angular/core";
import { SuiComponentFactory } from "../util/component-factory.service";
import { SuiMessageGlobalContainer, MessagePosition } from "./message-global-container";
import { MessageController, IMessageController } from "./message-controller";
import { MessageConfig } from "./message-config";
import { SuiActiveMessage } from "./active-message";

@Injectable()
export class SuiMessageService implements IMessageController {
private _controller:MessageController;
private _containerRef:ComponentRef<SuiMessageGlobalContainer>;

private get _container():SuiMessageGlobalContainer {
return this._containerRef.instance;
}

public get position():MessagePosition {
return this._container.position;
}

public set position(position:MessagePosition) {
this._container.position = position;
}

public get width():number {
return this._container.width;
}

public set width(width:number) {
this._container.width = width;
}

public get maxShown():number {
return this._controller.maxShown;
}

public set maxShown(max:number) {
this._controller.maxShown = max;
}

public get isNewestOnTop():boolean {
return this._controller.isNewestOnTop;
}

public set isNewestOnTop(value:boolean) {
this._controller.isNewestOnTop = value;
}

constructor(private _componentFactory:SuiComponentFactory) {
this._controller = new MessageController();

this._containerRef = this._componentFactory.createComponent(SuiMessageGlobalContainer);
this._container.controller = this._controller;

this._componentFactory.attachToApplication(this._containerRef);
this._componentFactory.moveToDocumentBody(this._containerRef);

this.position = MessagePosition.TopRight;
this.width = 480;
}

public show(config:MessageConfig):SuiActiveMessage {
return this._controller.show(config);
}

public dismissAll():void {
return this._controller.dismissAll();
}
}
Loading