Skip to content
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

Vltava - Data Binding using MatchMaker #1213

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 0 additions & 2 deletions examples/components/spaces/src/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ const textStyle: React.CSSProperties = {
cursor: "pointer",
};

export const ButtonName = "button";

/**
* Clicker example using view interfaces and stock component classes.
*/
Expand Down
105 changes: 105 additions & 0 deletions examples/components/vltava/src/components/button/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import {
PrimedComponent,
PrimedComponentFactory,
} from "@microsoft/fluid-aqueduct";
import {
IComponent,
IComponentHTMLVisual,
} from "@microsoft/fluid-component-core-interfaces";

import * as React from "react";
import * as ReactDOM from "react-dom";
import { IComponentDiscoverableInterfaces } from "@microsoft/fluid-framework-interfaces";
import { IComponentClicks } from "../../interfaces/clicker";

const buttonStyle: React.CSSProperties = {
WebkitUserSelect: "none", // Chrome-Safari
MozUserSelect: "none", // Firefox
msUserSelect: "none", //IE10+
textAlign:"center",
width: "100%",
height:"100%",
boxSizing: "border-box",
border:"1px solid black",
cursor: "pointer",
};

const textStyle: React.CSSProperties = {
WebkitUserSelect: "none", // Chrome-Safari
MozUserSelect: "none", // Firefox
msUserSelect: "none", //IE10+
display:"inline-block",
cursor: "pointer",
};

interface IButton {
click();
}

/**
* Button is a simple component that is just a button. It registers with the matchMaker so
* when the button is pressed Components that consume clicks can do work
*/
export class Button extends PrimedComponent
heliocliu marked this conversation as resolved.
Show resolved Hide resolved
implements
IButton,
IComponentHTMLVisual,
IComponentDiscoverableInterfaces,
IComponentClicks
{
private readonly registeredCallbacks: (() => void)[] = [];

private static readonly factory = new PrimedComponentFactory(Button, []);

public static getFactory() {
return Button.factory;
}

public get IComponentHTMLVisual() { return this; }

public get IComponentDiscoverableInterfaces() { return this; }

public get IComponentClicks() { return this; }

public get discoverableInterfaces(): (keyof IComponent)[] {
return [
"IComponentClicks",
];
}

public onClick(fn: () => void) {
this.registeredCallbacks.push(fn);
}

public click() {
this.registeredCallbacks.forEach((fn) => {
fn();
});
}

protected async componentHasInitialized() {
const matchMaker = await this.getService<IComponent>("matchMaker");
const interfaceRegistry = matchMaker.IComponentInterfacesRegistry;
if (interfaceRegistry) {
interfaceRegistry.registerComponentInterfaces(this);
}
}

/**
* If someone just calls render they are not providing a scope and we just pass
* undefined in.
*/
public render(div: HTMLElement) {
ReactDOM.render(
<div style={buttonStyle} onClick={this.click.bind(this)}>
<h1 style={textStyle}>+</h1>
</div>,
div);
}

}
6 changes: 6 additions & 0 deletions examples/components/vltava/src/components/button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

export * from "./button";
2 changes: 2 additions & 0 deletions examples/components/vltava/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
*/

export * from "./anchor";
export * from "./button";
export * from "./number";
export * from "./tabs";
export * from "./vltava";
6 changes: 6 additions & 0 deletions examples/components/vltava/src/components/number/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

export * from "./number";
154 changes: 154 additions & 0 deletions examples/components/vltava/src/components/number/number.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { EventEmitter } from "events";

import {
PrimedComponent,
PrimedComponentFactory,
} from "@microsoft/fluid-aqueduct";
import {
IComponent,
IComponentHTMLVisual,
} from "@microsoft/fluid-component-core-interfaces";
import { CounterValueType, Counter } from "@microsoft/fluid-map";
import { IComponentDiscoverInterfaces } from "@microsoft/fluid-framework-interfaces";

import * as React from "react";
import * as ReactDOM from "react-dom";

export const NumberName = "number";

const numberStyle: React.CSSProperties = {
textAlign:"center",
width: "100%",
height:"100%",
boxSizing: "border-box",
border:"1px solid black",
};

interface INumber extends EventEmitter {
value: number;
on(event: "incremented", listener: (value: number) => void): this;
}

/**
* Number clicker example using view interfaces and stock component classes.
*/
export class Number extends PrimedComponent
implements
IComponentHTMLVisual,
INumber,
IComponentDiscoverInterfaces
{
private counter: Counter;

public get IComponentHTMLVisual() { return this; }

public get IComponentDiscoverInterfaces() { return this; }

public get value() {
return this.counter.value;
}

public get interfacesToDiscover(): (keyof IComponent)[] {
return [
"IComponentClicks",
];
}

private static readonly factory = new PrimedComponentFactory(Number, []);

public static getFactory() {
return Number.factory;
}

public increment() {
this.counter.increment(1);
}

public notifyComponentsDiscovered(interfaceName: keyof IComponent, components: readonly IComponent[]): void {
components.forEach((component) => {
if (!component[interfaceName]) {
console.log(`component doesn't support interface ${interfaceName}`);
}

switch(interfaceName) {
case "IComponentClicks": {
const clicks = component.IComponentClicks;
if (clicks) {
clicks.onClick(this.increment.bind(this));
}
}
default:
}
});
}

public on(event: "incremented", listener: (value: number) => void): this;
public on(event: string | symbol, listener: (...args: any[]) => void): this {
return super.on(event, listener);
}

protected async componentInitializingFirstTime(){
this.root.createValueType("clicker-count", CounterValueType.Name, 0);
}

protected async componentHasInitialized() {
this.counter = this.root.get<Counter>("clicker-count");

this.counter.on("incremented", (_, currentValue: number) => {
this.emit("incremented", currentValue);
});

const matchMaker = await this.getService<IComponent>("matchMaker");
const interfaceRegistry = matchMaker.IComponentInterfacesRegistry;
if (interfaceRegistry) {
interfaceRegistry.registerComponentInterfaces(this);
}
}

/**
* Will return a new Clicker view
*/
public render(div: HTMLElement) {
ReactDOM.render(
<NumberView dataModel={this}/>,
div,
);
}
}

interface INumberViewProps {
dataModel: INumber;
}

interface INumberViewState {
value: number;
}

class NumberView extends React.Component<INumberViewProps, INumberViewState>{
constructor(props: INumberViewProps){
super(props);

this.state = {
value: this.props.dataModel.value,
};
}

componentDidMount() {
this.props.dataModel.on("incremented", (currentValue: number) =>{
this.setState({value:currentValue});
});
}

render(){
return (
<div style={numberStyle}>
<h1 style={{display:"inline-block"}}>{this.state.value}</h1>
</div>
);
}
}
7 changes: 7 additions & 0 deletions examples/components/vltava/src/components/tabs/dataModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,19 @@ export interface ITabsDataModel extends EventEmitter{
getTabIds(): string[];
createTab(type: string): Promise<string>;
getNewTabTypes(): ITabsTypes[];

on(event: "newTab", listener: (local: boolean) => void): this;
}

export class TabsDataModel extends EventEmitter implements ITabsDataModel {

private readonly tabs: IDirectory;

public on(event: "newTab", listener: (local: boolean) => void): this;
public on(event: string | symbol, listener: (...args: any[]) => void): this {
return super.on(event, listener);
}

constructor(
root: ISharedDirectory,
private readonly internalRegistry: IComponentRegistryDetails,
Expand Down
5 changes: 3 additions & 2 deletions examples/components/vltava/src/components/vltava/dataModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@ import {
import { ISharedDirectory } from "@microsoft/fluid-map";
import {
IQuorum,
ISequencedClient,
} from "@microsoft/fluid-protocol-definitions";

export interface IVltavaDataModel extends EventEmitter {
getDefaultComponent(): Promise<IComponent>;
getTitle(): string;
getUsers(): string[];

on(event: "membersChanged", listener: (users: string[]) => void): this;
}

export class VltavaDataModel extends EventEmitter implements IVltavaDataModel {
private readonly quorum: IQuorum;

public on(event: "membersChanged", listener: (users: Map<string, ISequencedClient>) => void): this;
public on(event: "membersChanged", listener: (users: string[]) => void): this;
public on(event: string | symbol, listener: (...args: any[]) => void): this {
return super.on(event, listener);
}
Expand Down
Loading