Skip to content

Commit

Permalink
Refactor Vue components using Composition API #230
Browse files Browse the repository at this point in the history
- Migrate `StatefulVue`:
  - Introduce `UseCollectionState` that replaces its behavior and acts
    as a shared state store.
  - Add more encapsulated, granular functions based on read or write
    access to state in CollectionState.
- Some linting rules get activates due to new code-base compability to
  modern parses, fix linting errors.
  - Rename Dialog to ModalDialog as after refactoring,
    eslintvue/no-reserved-component-names does not allow name Dialog.
  - To comply with `vue/multi-word-component-names`, rename:
    - `Code`          -> `CodeInstruction`
    - `Handle`        -> `SliderHandle`
    - `Documentable`  -> `DocumentableNode`
    - `Node`          -> `NodeContent`
    - `INode`         -> `INodeContent`
    - `Responsive`    -> `SizeObserver`
- Remove `vue-property-decorator` and `vue-class-component`
  dependencies.
- Refactor `watch` with computed properties when possible for cleaner
  code.
  - Introduce `UseApplication` to reduce repeated code in new components
    that use `computed` more heavily than before.
- Change TypeScript target to `es2017` to allow top level async calls
  for getting application context/state/instance to simplify the code by
  removing async calls. However, mocha (unit and integration) tests do
  not run with top level awaits, so a workaround is used.
  • Loading branch information
undergroundwires committed Aug 7, 2023
1 parent 3a594ac commit 1b9be8f
Show file tree
Hide file tree
Showing 67 changed files with 2,126 additions and 1,258 deletions.
4 changes: 3 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Application layer depends on and consumes domain layer. [Presentation layer](./p

State handling uses an event-driven subscription model to signal state changes and special functions to register changes. It does not depend on third party packages.

The presentation layer can read and modify state through the context. State changes trigger events that components can subscribe to for reactivity.

Each layer treat application layer differently.

![State](./../img/architecture/app-state.png)
Expand All @@ -45,7 +47,7 @@ Each layer treat application layer differently.
- So state is mutable, and fires related events when mutated.
- 📖 Read more: [application.md | Application state](./application.md#application-state).

It's comparable with flux ([`redux`](https://redux.js.org/)) or flux-like ([`vuex`](https://vuex.vuejs.org/)) patterns. Flux component "view" is [presentation layer](./presentation.md) in Vue. Flux functions "dispatcher", "store" and "action creation" functions lie in the [application layer](./application.md). A difference is that application state in privacy.sexy is mutable and lies in single flux "store" that holds app state and logic. The "actions" mutate the state directly which in turns act as dispatcher to notify its own event subscriptions (callbacks).
It's comparable with `flux`, `vuex`, and `pinia`. A difference is that mutable application layer state in privacy.sexy is mutable and lies in single "store" that holds app state and logic. The "actions" mutate the state directly which in turns act as dispatcher to notify its own event subscriptions (callbacks).

## AWS infrastructure

Expand Down
46 changes: 25 additions & 21 deletions docs/presentation.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Presentation layer

Presentation layer consists of UI-related code. It uses Vue.js as JavaScript framework and includes Vue.js components. It also includes [Electron](https://www.electronjs.org/) to provide functionality to desktop application.
The presentation layer handles UI concerns using Vue as JavaScript framework and Electron to provide desktop functionality.

It's designed event-driven from bottom to top. It listens user events (from top) and state events (from bottom) to update state or the GUI.
It reflects the [application state](./application.md#application-state) and allows user interactions to modify it. Components manage their own local UI state.

The presentation layer uses an event-driven architecture for bidirectional reactivity between the application state and UI. State change events flow bottom-up to trigger UI updates, while user events flow top-down through components, some ultimately modifying the application state.

📖 Refer to [architecture.md (Layered Application)](./architecture.md#layered-application) to read more about the layered architecture.

Expand All @@ -12,6 +14,7 @@ It's designed event-driven from bottom to top. It listens user events (from top)
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that other components share.
- [**`hooks`**](../src/presentation/components/Shared/Hooks): Shared hooks for state access
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets that webpack will process.
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles used throughout different components.
Expand All @@ -32,7 +35,7 @@ Add visual clues for clickable items. It should be as clear as possible that the

## Application data

Components (should) use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain to avoid [parsing and compiling](./application.md#parsing-and-compiling) the application again.
Components (should) use [`UseApplication`](./../src/presentation/components/Shared/Hooks/UseApplication.ts) to reach the application domain to avoid [parsing and compiling](./application.md#parsing-and-compiling) the application again.

[Application.ts](../src/domain/Application.ts) is an immutable domain model that represents application state. It includes:

Expand All @@ -43,32 +46,33 @@ You can read more about how application layer provides application data to he pr

## Application state

Inheritance of a Vue components marks whether it uses application state . Components that does not handle application state extends `Vue`. Stateful components mutate or/and react to state changes (such as user selection or search queries) in [ApplicationContext](./../src/application/Context/ApplicationContext.ts) extend [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) class to access the context / state.
This project uses a singleton instance of the application state, making it available to all Vue components.

The decision to not use third-party state management libraries like [`vuex`](https://web.archive.org/web/20230801191617/https://vuex.vuejs.org/) or [`pinia`](https://web.archive.org/web/20230801191743/https://pinia.vuejs.org/) was made to promote code independence and enhance portability.

Stateful components can mutate and/or react to state changes (e.g., user selection, search queries) in the [ApplicationContext](./../src/application/Context/ApplicationContext.ts). Vue components import [`CollectionState.ts`](./../src/presentation/components/Shared/CollectionState.ts) to access both the application context and the state.

[`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) functions include:
[`CollectionState.ts`](./../src/presentation/components/Shared/CollectionState.ts) provides several functionalities including:

- Creating a singleton of the state and makes it available to presentation layer as single source of truth.
- Providing virtual abstract `handleCollectionState` callback that it calls when
- the Vue loads the component,
- and also every time when state changes.
- Providing `events` member to make lifecycling of state subscriptions events easier because it ensures that components unsubscribe from listening to state events when
- the component is no longer used (destroyed),
- an if [ApplicationContext](./../src/application/Context/ApplicationContext.ts) changes the active [collection](./collection-files.md) to a different one.
- **Singleton State Instance**: It creates a singleton instance of the state, which is shared across the presentation layer. The singleton instance ensures that there's a single source of truth for the application's state.
- **State Change Callback and Lifecycle Management**: It offers a mechanism to register callbacks, which will be invoked when the state initializes or mutates. It ensures that components unsubscribe from state events when they are no longer in use or when [ApplicationContext](./../src/application/Context/ApplicationContext.ts) switches the active [collection](./collection-files.md).
- **State Access and Modification**: It provides functions to read and mutate for accessing and modifying the state, encapsulating the details of these operations.
- **Event Subscription Lifecycle Management**: Includes an `events` member that simplifies state subscription lifecycle events. This ensures that components unsubscribe from state events when they are no longer in use, or when [ApplicationContext](./../src/application/Context/ApplicationContext.ts) switches the active [collection](./collection-files.md).

📖 Refer to [architecture.md | Application State](./architecture.md#application-state) to get an overview of event handling and [application.md | Application State](./presentation.md#application-state) for deeper look into how the application layer manages state.
📖 Refer to [architecture.md | Application State](./architecture.md#application-state) for an overview of event handling and [application.md | Application State](./presentation.md#application-state) for an in-depth understanding of state management in the application layer.

## Modals

[Dialog.vue](./../src/presentation/components/Shared/Dialog.vue) is a shared component that other components used to show modal windows.
[ModalDialog.vue](./../src/presentation/components/Shared/ModalDialog.vue) is a shared component utilized for rendering modal windows.

You can use it by wrapping the content inside of its `slot` and call `.show()` function on its reference. For example:
Use the component by wrapping the desired content within its slot and calling the .show() function on its reference, as shown below:

```html
<Dialog ref="testDialog">
<div>Hello world</div>
</Dialog>
<div @click="$refs.testDialog.show()">Show dialog</div>
```
```html
<ModalDialog ref="testDialog">
<div>Hello world</div>
</ModalDialog>
<div @click="$refs.testDialog.show()">Show dialog</div>
```

## Sass naming convention

Expand Down
35 changes: 2 additions & 33 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 3 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@
"npm": "^9.8.1",
"v-tooltip": "2.1.3",
"vue": "^2.7.14",
"vue-class-component": "^7.2.6",
"vue-js-modal": "^2.0.1",
"vue-property-decorator": "^9.1.2"
"vue-js-modal": "^2.0.1"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.2",
Expand Down Expand Up @@ -85,10 +83,10 @@
"sass-loader": "^13.3.2",
"svgexport": "^0.4.2",
"ts-loader": "^9.4.4",
"tslib": "~2.4.0",
"typescript": "~4.6.2",
"vue-cli-plugin-electron-builder": "^3.0.0-alpha.4",
"yaml-lint": "^1.7.0",
"tslib": "~2.4.0"
"yaml-lint": "^1.7.0"
},
"overrides": {
"vue-cli-plugin-electron-builder": {
Expand Down
8 changes: 3 additions & 5 deletions src/presentation/components/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,23 @@
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { defineComponent } from 'vue';
import TheHeader from '@/presentation/components/TheHeader.vue';
import TheFooter from '@/presentation/components/TheFooter/TheFooter.vue';
import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue';
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
@Component({
export default defineComponent({
components: {
TheHeader,
TheCodeButtons,
TheScriptArea,
TheSearchBar,
TheFooter,
},
})
export default class App extends Vue {
});
}
</script>

<style lang="scss">
Expand Down
40 changes: 28 additions & 12 deletions src/presentation/components/Code/CodeButtons/IconButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,36 @@
</template>

<script lang="ts">
import {
Component, Prop, Emit, Vue,
} from 'vue-property-decorator';
import { defineComponent } from 'vue';
@Component
export default class IconButton extends Vue {
@Prop() public text!: number;
@Prop() public iconPrefix!: string;
@Prop() public iconName!: string;
export default defineComponent({
props: {
text: {
type: String,
required: true,
},
iconPrefix: {
type: String,
required: true,
},
iconName: {
type: String,
required: true,
},
},
emits: [
'click',
],
setup(_, { emit }) {
function onClicked() {
emit('click');
}
@Emit('click') public onClicked() { /* do nothing except firing event */ }
}
return {
onClicked,
};
},
});
</script>

<style scoped lang="scss">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,23 @@
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { defineComponent, useSlots } from 'vue';
import { Clipboard } from '@/infrastructure/Clipboard';
@Component
export default class Code extends Vue {
public copyCode(): void {
const code = this.$slots.default[0].text;
Clipboard.copyText(code);
}
}
export default defineComponent({
setup() {
const slots = useSlots();
function copyCode() {
const code = slots.default()[0].text;
Clipboard.copyText(code);
}
return {
copyCode,
};
},
});
</script>

<style scoped lang="scss">
Expand Down
Loading

0 comments on commit 1b9be8f

Please sign in to comment.